☰
Servlet+JSP+MySQL打造艺术品展览管理系统:从建表到部署全流程
2026/10/1 10:39:15 网站建设 项目流程

简介:一套基于Servlet+Jsp+MySQL的艺术作品展览管理系统源代码及配套数据库,面向Java Web初学者和需要搭建线上展览平台的管理者,解决艺术作品的数字化展示、展览信息维护与用户互动管理等问题。压缩包共包含50个文件,以18个Java源码、15个JSP页面、12个XML配置文件为主体,另有1个SQL数据库脚本,整体仅52KB,结构紧凑便于快速部署与二次开发。系统内置艺术作品表、展览信息表、用户信息表三大核心数据表,覆盖作品详情、展览日程与用户登录记录等关键模块,代码注释详尽,可直观学习Servlet请求处理、JSP页面渲染以及通过JDBC操作MySQL的完整流程。目前已有52人浏览学习,适合作为课程设计、毕业设计或入门实战的参考案例,在此基础上可方便地扩展在线评论、预约看展等个性化功能。

1. 一套能跑通全流程的艺术品展览管理系统:Servlet+JSP+MySQL怎么分工

搞过JavaWeb课设或毕设的人,对基于Servlet+Jsp+MySQL的艺术作品展览管理系统这个组合一定不陌生:一套标准的传统JavaWeb单体应用,用Servlet接请求、用JSP渲染页面、用MySQL存业务数据。它的价值在于足够简单、足够透明,每一个请求从浏览器到数据库再回到浏览器的路径都能被完整看到,特别适合用来理解JavaWeb的底层运行机制,也适合作为毕业设计、课程实训和入门项目的底座。

这套系统的业务范围一般包括展览信息发布、作品展示、用户注册登录、展览预约/购票、评论留言和后台管理。对新手来说,它比纯图书管理多了“展览—展品—艺术家—订单”这种多表关联的业务模型,比电商系统又少了复杂的支付和库存,复杂度刚好卡在能独立思考、又能在一两个星期内做出来的位置。本文按我做这类项目时的真实顺序来拆:先讲选型理由和工程骨架,再给数据库表结构和建表SQL,接着落Servlet和JSP的代码,最后把乱码、SSL连接、war打包这类高频坑一次说清。

2. 为什么这套三件套还值得做:运行结构、工程骨架与适用边界

2.1 技术选型不是怀旧:Servlet+JSP+MySQL的适用场景

很多人在选题时会犹豫:现在都Spring Boot了,为什么还要写Servlet和JSP?我一般会反问一句:你是想学框架,还是想学Web运行原理?Servlet+JSP这套东西,本质上就是Java Web最原始的运行模型——Servlet处理请求逻辑,JSP负责展示,JDBC连数据库。Spring MVC再高级,底层也是从一个DispatcherServlet开始的;MyBatis再方便,底层还是JDBC那一套Connection、Statement、ResultSet。把这个三件套做明白,后面看任何框架的源码和报错堆栈都能快很多。

适用人群上,这套方案是JavaWeb方向课设、毕设、实训项目的常客,这在很多实训平台和论坛的选题热度里可以看得很清楚。适用请求量上,单机部署、日均几百到几千次访问的展览信息站完全够用。它不适合高并发互联网产品,但作为教学项目、内部管理系统和个人作品集,它是成本和可解释性最平衡的方案。

2.2 标准的工程骨架:包结构怎么分才能好答辩

不用Maven,直接用IDEA或Eclipse建一个Dynamic Web Project,包结构按“控制层—业务层—数据访问层—工具类”划分即可。我一般会建这样一个目录:

src/ ├── com.artexpo.servlet # 控制层:登录、注册、展览、展品、订单 ├── com.artexpo.service # 业务层:参数校验、业务规则 ├── com.artexpo.dao # 数据访问层:JDBC操作 ├── com.artexpo.entity # 实体类:User、Exhibition、Artwork、Order ├── com.artexpo.util # 工具类:DBUtil、PageBean、StringUtil └── com.artexpo.filter # 过滤器:编码、登录验证 WebContent/ ├── index.jsp # 入口首页 ├── login.jsp / register.jsp ├── exhibition_list.jsp ├── exhibition_detail.jsp ├── artwork_detail.jsp ├── admin/ # 后台管理页面 └── WEB-INF/ └── web.xml

分层时最需要注意的一点是:Servlet里不要直接拼SQL,最少要经过一层Service。我见过很多同学把JDBC代码直接写在Servlet里,跑通没问题,但写第二个功能时就开始复制粘贴,改一个字段要改三处。分层不是为了装模作样,是为了让“展览列表”和“展览详情”这种相近功能复用同一套数据访问代码。

2.3 两种常见开发环境:IDEA与VSCode的配置差异

课堂演示大多用IDEA新建JSP项目,勾选Web Application模板后,IDEA会自动生成web.xml和WebContent结构,配好Tomcat后直接运行。用VSCode写Servlet则是另一种常见形态:需要手动安装Java Extension Pack、Tomcat for Java插件,还要在项目里加入servlet-api.jar作为编译依赖。VSCode轻量,适合写单文件调试;IDEA集成度高,适合完整跑项目。新人更推荐IDEA,省去不少环境折腾的时间。

3. 数据库设计先行:展览、展品、用户与订单的建表SQL和必调参数

3.1 需求到表结构:一个展览管理系统需要几张表

设计数据库时,我习惯先列出业务句子,再从中抽实体。这个系统里的核心业务句是:管理员发布展览;参展方提交作品参加某场展览;普通用户注册登录后查看展览和作品、预约观展、发表评论。抽出来就是六个实体:管理员、普通用户、参展方、展览、作品、预约/订单,再加上评论。

展览和作品是一对多,作品和参展方是多对一,用户和订单是一对多,订单和展览是多对一。设计表时把外键关系建清楚,后面做列表联查就不用在Java代码里写拼接逻辑。下面是这个系统最常用的四张核心表,我用MySQL 8.0来建:

-- 用户表:同时服务管理员与普通用户,用role字段区分 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT DEFAULT 1 COMMENT '1-普通用户,2-管理员,3-参展方', real_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), status TINYINT DEFAULT 0 COMMENT '0-正常,1-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 展览表 CREATE TABLE exhibition ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, theme VARCHAR(200), cover_img VARCHAR(500), intro TEXT, start_date DATE, end_date DATE, location VARCHAR(200), status TINYINT DEFAULT 0 COMMENT '0-未开始,1-进行中,2-已结束', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 作品表 CREATE TABLE artwork ( id INT PRIMARY KEY AUTO_INCREMENT, exhibition_id INT NOT NULL, artist_id INT, title VARCHAR(200) NOT NULL, category VARCHAR(50), image_url VARCHAR(500), description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_artwork_exhibition FOREIGN KEY (exhibition_id) REFERENCES exhibition(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约/订单表 CREATE TABLE visit_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, exhibition_id INT NOT NULL, visit_date DATE, ticket_count INT DEFAULT 1, status TINYINT DEFAULT 0 COMMENT '0-已预约,1-已取消,2-已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_order_exhibition FOREIGN KEY (exhibition_id) REFERENCES exhibition(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意这里有两个高频检索词直接对应的设置:字符集用utf8mb4而不是utf8,因为展品名称里可能包含生僻字、特殊符号甚至emoji;status字段设置默认值为0,避免插入数据时因漏填状态列而报错或写入空值。这也是很多真实项目里出现“状态字段为空导致列表判断失效”的根源之一。

3.2 JDBC连接参数:useSSL和characterEncoding两个必调项

MySQL 8.0的默认认证插件是caching_sha2_password,驱动要换成com.mysql.cj.jdbc.Driver,连接串里建议显式关闭SSL并指定字符集。很多新手直接连的时候,报错信息会指向SSL或时区问题,实际上排查一下连接串多半就能发现原因。这两个参数在JDBC连接串里必须写清楚:

private static final String URL = "jdbc:mysql://localhost:3306/art_expo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码";

allowPublicKeyRetrieval=true这个参数容易被忽略。MySQL 8.0用caching_sha2_password认证时,客户端第一次连接需要从服务端获取公钥,如果JDBC驱动版本较老或参数没放开,会直接抛出Public Key Retrieval is not allowed的异常。这三个参数加在一起,能解决90%的MySQL 8.0连接问题。

3.3 连接池选型:C3P0还是Druid

课设和毕设里最常见的连接池是C3P0和Druid。C3P0胜在配置简单,单独一个c3p0-config.xml文件就行,和Spring、Hibernate的整合资料很多;Druid胜在自带监控页面和SQL防火墙,适合作为项目亮点写进答辩。如果只是把系统跑通,C3P0够用;如果想让老师看到“数据库连接池监控”这个点,Druid的StatViewServlet能直接展示SQL执行次数和慢查询,加分效果明显。

下面是我的C3P0配置写法,直接放在src根目录的c3p0-config.xml里:

<c3p0-config> <named-config name="artexpo"> <property name="jdbcUrl">jdbc:mysql://localhost:3306/art_expo?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;characterEncoding=utf8</property> <property name="user">root</property> <property name="password">123456</property> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <property name="initialPoolSize">5</property> <property name="minPoolSize">5</property> <property name="maxPoolSize">20</property> <property name="maxIdleTime">60</property> <property name="acquireIncrement">2</property> <property name="checkoutTimeout">5000</property> </named-config> </c3p0-config>

参数含义和调整逻辑:initialPoolSize和minPoolSize保持5就行,系统启动时会预建5个连接;maxPoolSize设20,如果并发预约的峰值较高可以调到50,但注意MySQL默认max_connections是151,连接池上限不能超过数据库上限;checkoutTimeout是获取连接的超时时间,设5000毫秒,超过就报错而不是无限等待;maxIdleTime设置连接空闲回收时间,防止数据库wait_timeout把空闲连接切断后,连接池还继续往出拿。数据库的wait_timeout默认28800秒,也就是8小时,如果连接池空闲超过这个时间,连接池里的连接会被MySQL服务端断开,这时第一次请求会报连接失效。maxIdleTime低于wait_timeout即可,60秒是个稳妥值。

3.4 初始化数据:一个可直接执行的SQL脚本

建完表后,要插入一笔管理员账号和一场测试展览,不然项目跑起来登录入口都没有。管理员密码我建议先用明文,跑通登录后再改成SHA-256加盐。下面这段初始化脚本可以直接执行:

INSERT INTO sys_user (username, password, role, real_name) VALUES ('admin', '123456', 2, '系统管理员'); INSERT INTO exam (title, theme, start_date, end_date, location) VALUES ('2024当代青年艺术邀请展', '青年·城市·未来', '2024-06-01', '2024-08-31', '市美术馆3号展厅');

如果用的是MySQL 5.7以前的版本,utf8mb4在个别字符集配置上会有限制,建议优先选择MySQL 8.0,并按3.1节的表结构来建,避免后续排序规则不一致导致的联查报错。

4. 代码怎么落地:连接工具、访问控制和JSP页面的分工实现

4.1 DBUtil连接工具类:把获取连接和关闭连接收口

数据库操作的第一步是一个统一的DBUtil。它的职责只有两个:从C3P0连接池拿连接、统一释放资源。这样每个DAO方法里不需要重复写Class.forName、getConnection和try-catch-finally,代码量能减少三分之一,也避免忘记关闭连接导致的连接池耗尽。

package com.artexpo.util; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.mchange.v2.c3p0.ComboPooledDataSource; public class DBUtil { private static ComboPooledDataSource cpds = new ComboPooledDataSource("artexpo"); public static Connection getConnection() throws SQLException { return cpds.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } public static void close(Connection conn, Statement stmt) { close(conn, stmt, null); } }

注意这里用的是ComboPooledDataSource的无参构造加new ComboPooledDataSource("artexpo"),这个字符串参数必须与c3p0-config.xml里named-config的name属性一致,否则会走默认配置而不是我们自定义的池参数。关闭连接时,从连接池拿出的Connection调用close()并不是真正断开数据库连接,而是把连接归还给池子,所以即使频繁开关连接也不用担心性能。

4.2 登录与会话管理:Servlet接管请求,Session记录登录态

登录功能是整个系统安全性的第一道门。常见做法是:LoginServlet在doPost里接收username和password,调用UserService的login方法查询数据库,验证成功后把用户信息放进session,然后重定向到首页;验证失败则请求转发回login.jsp并携带错误提示。下面是一个可参考的实现:

package com.artexpo.servlet; import com.artexpo.dao.UserDao; import com.artexpo.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") 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"); if (username == null || password == null || username.trim().isEmpty() || password.trim().isEmpty()) { req.setAttribute("error", "用户名和密码不能为空"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

几个参数说明:req.setCharacterEncoding("UTF-8")必须在读取任何参数之前调用,放在最前面;session.setMaxInactiveInterval(30 * 60)设置闲置超时30分钟,单位是秒;登录失败用forward转发而不是sendRedirect,因为forward能携带request域里的error属性,而redirect会丢失;登录成功用sendRedirect,是为了避免刷新页面时表单重复提交。这是一个非常典型的Post/Redirect/Get模式,答辩时可以作为“如何避免表单重复提交”的答案。

4.3 展览列表与模糊查询:传参、联查和页面渲染

展览列表页是首页的核心呈现。通过在ExhibitionDao里写一个支持多条件查询的方法,可以将标题关键词、展览状态作为可选参数传入。JDBC里拼接动态SQL时,用StringBuilder逐个追加条件并绑定参数,能防止字段名被注入。

public List<Exhibition> searchExhibitions(String keyword, String status) { List<Exhibition> list = new ArrayList<>(); StringBuilder sql = new StringBuilder( "SELECT e.*, COUNT(a.id) AS artwork_count FROM exhibition e " + "LEFT JOIN artwork a ON e.id = a.exhibition_id "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append("WHERE e.title LIKE ? "); params.add("%" + keyword.trim() + "%"); } if (status != null && !status.trim().isEmpty()) { sql.append(keyword == null || keyword.trim().isEmpty() ? "WHERE " : "AND "); sql.append("e.status = ? "); params.add(Integer.parseInt(status)); } sql.append("GROUP BY e.id ORDER BY e.create_time DESC"); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Exhibition e = new Exhibition(); e.setId(rs.getInt("id")); e.setTitle(rs.getString("title")); e.setStartDate(rs.getDate("start_date")); e.setEndDate(rs.getDate("end_date")); e.setLocation(rs.getString("location")); e.setArtworkCount(rs.getInt("artwork_count")); list.add(e); } } } catch (SQLException e) { e.printStackTrace(); } return list; }

这段代码里用到了LEFT JOIN和GROUP BY,画出作品的统计数量。要注意:使用LEFT JOIN时,即使某场展览还没有作品,artwork_count也会返回0,而不会丢行。LIKE模糊查询在关键词两侧拼接%号,匹配任意位置的字符,这种写法无法利用普通索引,但对于课设级数据量完全够用。用PreparedStatement的setObject逐个绑定参数,尽量不拼接字符串,这样能避免SQL注入。

4.4 JSP列表页:JSTL遍历比脚本片段更规范

JSP页面里直接写<% for(...) { %>不是不行,但是页面会很乱。建议引入JSTL的c:forEach遍历集合,页面结构与HTML风格更统一。先在WebContent/WEB-INF/lib下引入jstl-1.2.jar,然后在页面顶部写taglib指令。

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <table class="table"> <thead> <tr><th>展览名称</th><th>展期</th><th>地点</th><th>作品数</th><th>操作</th></tr> </thead> <tbody> <c:forEach items="${exhibitionList}" var="item"> <tr> <td>${item.title}</td> <td> <fmt:formatDate value="${item.startDate}" pattern="yyyy-MM-dd" /> 至 <fmt:formatDate value="${item.endDate}" pattern="yyyy-MM-dd" /> </td> <td>${item.location}</td> <td>${item.artworkCount}</td> <td> <a href="${pageContext.request.contextPath}/exhibition/detail?id=${item.id}">查看详情</a> <a href="${pageContext.request.contextPath}/order/create?exhibitionId=${item.id}">预约观展</a> </td> </tr> </c:forEach> </tbody> </table>

页面里所有的动态链接都加了${pageContext.request.contextPath},也就是部署后的应用上下文路径。不加这个前缀,在IDEA内置Tomcat里跑是正常的,一旦把war包丢到Tomcat的webapps下,路径就变成/art_expo开头,原来的链接全部404。这是一条值得记住的细节。fmt:formatDate用来格式化日期,避免页面直接显示数据库里的yyyy-MM-dd HH:mm:ss原始值。GET请求访问详情页时,记得在Servlet中设置响应编码为UTF-8,防止中文标题出现乱码。

4.5 对比一下:直接JDBC与JPA方案的取舍

这套系统还可以用MyBatis或Spring Data JPA来做数据访问,代码量会少一些,但调试时会发现报错堆栈直接指向框架内部。直接用JDBC的最大好处是:每一个SQL都看得见摸得着,出了错定位到具体哪一行几乎一目了然,这对第一次接触JavaWeb的人来说是非常必要的学习过程。做工程化应用再上MyBatis,做这套系统时先用基本功把它写透,是更合理的路径。

5. 传统JSP项目避坑:从乱码到SSL连接再到war打包的5个现场记录

5.1 MySQL SSL连接错误:c3p0启动时明明配置没问题,运行却报SSL异常

现象:Tomcat启动不报错,第一次访问登录接口时控制台抛出Communications link failure或SSLHandshakeException,提示信息长到看不清具体原因。

原因:MySQL 8.0服务端默认开启SSL,客户端驱动默认也会尝试协商加密连接。如果服务端的SSL证书不受信任,握手阶段就会失败。c3p0-config.xml里jdbcUrl如果没写useSSL=false,驱动会尝试开启SSL。

解决:在c3p0-config.xml的jdbcUrl中写入useSSL=false&allowPublicKeyRetrieval=true,同时确认serverTimezone与本地时区一致,比如serverTimezone=Asia/Shanghai。这是该报错最常见的两种结果。修改后重启Tomcat即可。这个坑在MySQL 8.0出现后非常典型,很多人以为是驱动问题,实际上驱动没做错什么,只是没有显式告诉它“这里不需要SSL”。

5.2 页面中文全是问号:JSP、Servlet、数据库三层各有一套编码

现象:浏览器里展示的展览名称显示为?????,但数据库中实际存的是正确中文。

原因:编码不统一。JSP页面pageEncoding和contentType不一致,Servlet里没设置req.setCharacterEncoding("UTF-8"),数据库连接串里没带characterEncoding=utf8,三层各用各的编码,中文就会在某个环节变成问号。

解决:三层统一成UTF-8。JSP页面用<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>;Servlet的doPost开头写req.setCharacterEncoding("UTF-8");数据库连接串加characterEncoding=utf8;MySQL表的CHARACTER SET在3.1节已经设置成utf8mb4。四件事做完,乱码问题基本绝迹。也可以用Filter统一处理编码,写一个EncodingFilter类,在web.xml里配置/*路径,比在每个Servlet里手动设置更省事。

5.3 web.xml版本与Servlet注解冲突:访问Servlet报404或500

现象:项目跑起来后,首页能打开,但所有Servlet请求都返回404,或控制台提示ClassNotFoundException。

原因:IDEA新建Dynamic Web Project时会生成一个web.xml,不同版本对应的Servlet规范不一样。Servlet 3.0以上支持@WebServlet注解,如果web.xml版本是2.5,注解可能不生效;反之,如果既用了注解又在web.xml里重复配置了同一个URL映射,容器会报映射冲突,部署失败。

解决:新建项目时选择Servlet 4.0或3.1版本,使用注解方式定义Servlet时,不要同时在web.xml里配置同一个servlet-mapping。如果是已有项目,统一点:要么全部用注解,要么全部用web.xml配置。还有一种情况是web.xml文件名或位置不对,必须放在WebContent/WEB-INF/web.xml,IDEA老版本偶尔创建项目失败,右键项目添加Web Facets后重新生成即可。

5.4 JSP页面过大导致编译失败:too large的报错

现象:后台管理页面内容很多,启动不报错,首次访问该页面时Tomcat报org.apache.jasper.JasperException: JSP file too large,或者直接抛APPLICATION_ALLOCATED_PAGE_TOO_LARGE。

原因:JSP翻译成Java文件时,Tomcat默认对单个JSP文件大小有限制。早期Tomcat版本中,单个JSP超过64KB就会出问题。

解决:把页面拆分成多个片段。常见做法是把公共的页头、页脚做成include片段,用<%@ include file="common/header.jsp" %>静态引入;列表页拆成查询条件区、表格区、分页区三个jsp片段。数据库字段用TEXT类型存储长文本信息时,页面中不要一次性全部输出,用c:out配合maxLength省略号截断显示,需要完整内容时再点击查看详情。

5.5 传统JSP项目打包war:IDEA里打好的包部署后访问404

现象:在IDEA里直接右键项目选择Build Artifacts打出war包,丢进Tomcat的webapps目录后,启动正常但访问时404,或者页面样式全丢。

原因:war包的目录结构不对,或者项目上下文路径和浏览器里访问的路径不匹配。IDEA中Artifacts设置里如果没有配置Web Facet的war exploded结构,打出来的是扁平目录,而不是符合Tomcat要求的WEB-INF/classes、WEB-INF/lib标准结构。

解决:在IDEA的Project Structure里配置Artifacts,选择Web Application: Archive,确保Output directory里生成的war包解压后能看到WEB-INF目录。部署时把war包放到Tomcat的webapps目录,启动后Tomcat会自动解压,上下文默认是war包的文件名。如果你在IDEA里部署时没有指定Application context,浏览器里访问要用http://localhost:8080/项目名/index.jsp而不是直接访问/index.jsp。这个坑在传统JSP项目打包war的场景中出现频率极高,建议打包前先查看war包内的目录结构,确认无误再部署。

6. 交付前验证:并发测试盯Servlet生命周期,然后按套路打包上线

6.1 用JMeter做一次带断言的并发请求验证

项目能跑通和能扛住并发是两回事。我习惯在交付前用JMeter压一次登录和展览列表接口,不需要复杂场景,一个线程组、一个HTTP请求、一个断言就够了。

jmeter -n -t LoginTest.jmx -l result.jtl -e -o report/

JMeter脚本里设置线程数50、循环1次、ramp-up时间5秒,并发打登录接口。断言里检查响应文本是否包含“欢迎”或“首页”的关键词,如果断言失败,说明登录逻辑或数据库连接池在高并发下出了问题。这个测试能直接暴露三个坑:连接池最大连接数不够时会报Connection is not available, request timed out,单条SQL执行太慢导致线程全部阻塞,Session并发写冲突导致登录态丢失。JMeter是为了给答辩的时候拿出一份压测报告,更是为了在交付前把隐患全部暴露在自己的电脑上。

6.2 用Tomcat日志观测Servlet生命周期

Servlet的生命周期由容器管理:第一次请求时执行init()初始化,之后每个请求走service(),容器关闭时执行destroy()。验证它最直观的方式是在Servlet里输出日志,然后观察调用顺序。

@Override public void init() throws ServletException { System.out.println("LoginServlet init 被调用"); super.init(); } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("LoginServlet service 处理请求: " + req.getRequestURI()); super.service(req, resp); } @Override public void destroy() { System.out.println("LoginServlet destroy 被调用"); super.destroy(); }

三个方法各自打印一行日志,然后连续访问两次登录接口,观察Tomcat控制台:init只出现一次,service出现两次,重启Tomcat时看到destroy。这样Servlet生命周期就不再是一个抽象概念,而是一个看得到的过程。很多面试题问生命周期,本质上问的就是这个观测结果——什么时候只执行一次,什么时候每次执行,什么时候回收资源。

6.3 上线前的配置检查清单

我每次交付项目前都按这个固定套路过一遍,主要用一张清单来对照:数据源配置里的密码是否已经改成生产库的密码;c3p0-config.xml的maxPoolSize和数据库max_connections是否匹配;jdbcUrl里useSSL、characterEncoding、serverTimezone三个参数是否齐全;web.xml里welcome-file是否指向项目入口页面;Filter过滤器中排除登录页和静态资源;所有页面中的href是否都带pageContext.request.contextPath前缀;打war包前查看Artifacts目录是否是标准的WEB-INF结构;部署到Tomcat前确认context路径正确。其中最后一条最常见:本地IDEA跑的是应用上下文/art_expo,部署后访问地址不对,于是404。

我自己的习惯是,先用JMeter压过再打war,因为有些并发问题必须在多线程场景下才会出现,光靠浏览器点几次看不出来。做完这个系统之后,再看Spring Boot的自动配置,会明白那些“约定大于配置”的背后,到底解决了早期JavaWeb的哪些痛点。希望这篇笔记能帮你把这条路走通,也能在答辩时讲清楚每一个选择背后的原因。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询