☰
基于Servlet+Jsp+MySQL的艺术作品展览系统源码解析与部署指南
2026/10/1 10:39:25 网站建设 项目流程

简介:一套基于Servlet+Jsp+MySQL构建的艺术作品展览管理系统,源代码与数据库文件一并提供,面向Java Web初中级学习者及需要快速落地展览管理功能的开发者。系统设计了艺术作品、展览信息、用户信息三张核心数据表,支持注册登录、展览维护、作品分类检索与在线交流,完整覆盖数字化展览管理的基本业务链路。资源包共50个文件,包括18个java后台逻辑、15个jsp页面、12个xml配置、1个sql数据库脚本及说明文档,压缩包仅52KB,目录按Maven工程结构组织,配合README可快速了解项目构成。通过源码可深入理解JSP转Servlet的运作机制、JDBC连接MySQL的写法以及分层代码的组织方式;数据库脚本可直接初始化,便于本地运行和二次开发。已有52人学习,对课程设计、毕业设计或艺术展览线上化实践均具参考价值。

1. 一套“老技术栈”的毕设系统,为什么今天还值得照着跑一遍

如果你正在找一个能直接运行、能讲清楚、改起来不费劲的 JavaWeb 项目,“基于Servlet+Jsp+MySQL的艺术作品展览管理系统源代码+数据库”几乎是搜索“基于jsp的毕设选题”时躲不开的名字。这类系统做的事情不复杂:管理员登录后上传作品、按分类维护、决定上架还是下架,访客在前台浏览作品列表和详情,所有业务数据落在 MySQL 里,Servlet 负责接收请求和控制跳转,Jsp 负责把数据渲染成页面。放在今天看,Servlet+Jsp 这套栈没有 Spring Boot 的自动配置和注解魔法,但反过来说它也没有黑匣子;配合完整源代码和 SQL 脚本,新手能在一台干净电脑上从建库开始一步步跑通,熟手也能快速定位业务逻辑改需求。这篇文章就按我常用的落地顺序,把这个方案拆成从源码理解到部署、建库、排错、验证的完整路径。

2. 源码里没有“框架魔法”:Servlet、Jsp 与三层结构怎么分工

2.1 从 war 项目的目录结构读懂第一件事

拿到这种源代码,先别急着打开某个 Java 文件,而是看整个项目的目录结构。传统 Servlet+Jsp 项目的分包方式很固定,基本长这样:

项目根目录/ ├── src/ │ ├── com.xxx.controller # Servlet,负责接收请求和页面跳转 │ │ ├── AdminLoginServlet.java │ │ ├── ArtworkListServlet.java │ │ └── ArtworkAddServlet.java │ ├── com.xxx.service # 业务层,处理上传、上下架、统计等规则 │ │ ├── ArtworkService.java │ │ └── ArtworkServiceImpl.java │ ├── com.xxx.dao # 数据访问层,只和 SQL 打交道 │ │ ├── ArtworkDao.java │ │ └── ArtworkDaoImpl.java │ ├── com.xxx.entity # 实体类,对应数据库表 │ │ └── Artwork.java │ ├── com.xxx.util # JDBC 工具类、字符串处理等 │ │ ├── DBUtil.java │ │ └── StringUtil.java │ └── com.xxx.filter # 过滤器,编码处理、登录校验 │ └── EncodingFilter.java ├── web/ │ ├── admin/ │ │ ├── artwork_list.jsp │ │ └── artwork_add.jsp │ ├── index.jsp # 前台首页 │ ├── artwork_detail.jsp # 作品详情页 │ └── WEB-INF/ │ ├── web.xml # Servlet 映射、过滤器、欢迎页声明 │ └── lib/ # 项目依赖的 jar 包 └── sql/ └── artwork_exhibition.sql # 建库建表 + 初始数据脚本

凡是能把这些目录完整交付出来的源码,就算代码写得参差,业务跑通基本没问题。反过来,只丢几个散落的 Java 文件、没有 web.xml 和 sql 目录的“源代码”,你要花大量时间去猜测表结构和 URL 映射,那种项目我会直接放一边。

为什么要按 controller / service / dao 分包?核心目的是让每一层的职责能被单独验证。Servlet 只做三件事:收参数、调 Service、把结果放到 request 域里转发给 Jsp。真正查库的代码只在 DAO 层出现,这样以后从 MySQL 换成别的数据库、或者把查询逻辑改成分页,都不需要动页面。这种分层在 Spring Boot 里演化成了 @Controller、@Service、@Repository,如果你照着这份源码把每一层都读懂,后面学框架会轻松得多。

2.2 一次作品列表请求的完整流转

我一般会从列表页这个最普通的场景入手,因为一个请求从浏览器到数据库再回到浏览器的路径,直接展示出 Servlet、Jsp、MySQL 三者怎么配合。请求地址是/artwork/list,对应的 Servlet 大致长这样:

@WebServlet("/artwork/list") public class ArtworkListServlet extends HttpServlet { private ArtworkService artworkService = new ArtworkServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 接收分类参数,没传就查全部 String categoryId = req.getParameter("categoryId"); // 2. 业务逻辑交给 Service 层,Servlet 不直接写 SQL List<Artwork> list = artworkService.listArtworks(categoryId); // 3. 把结果塞进 request 域,转发给 Jsp 渲染 req.setAttribute("artworkList", list); req.getRequestDispatcher("/artwork_list.jsp").forward(req, resp); } }

这段代码有几个点值得细看。第一,@WebServlet注解是 Servlet 3.0 之后的写法,省去了在 web.xml 里一个个配<servlet>和<servlet-mapping>的重复劳动,也正好对应“servlet生命周期”里最常见的考点:容器启动时扫描注解完成注册,init()只执行一次,之后每次请求都走service()再分发到doGet或doPost,项目关闭时执行destroy()。

第二,这里用的是forward而不是sendRedirect。转发是服务器内部的跳转,Servlet 把artworkList放进了 request 域,同一个请求到达 Jsp 时还能用request.getAttribute("artworkList")取到数据;重定向会发起第二次 HTTP 请求,request 里的属性就丢了,届时就只能去 session 域取值。很多新手在这里改代码翻车,改完发现列表页空白,就是没想清楚转发和重定向的数据携带方式差异。

对应的 Jsp 页面,用 JSTL 标签渲染列表:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${artworkList}" var="a"> <div class="card"> <img src="${a.coverPath}" alt="${a.title}" /> <h3>${a.title}</h3> <p>作者:${a.author}</p> <a href="artwork/detail?id=${a.id}">查看详情</a> </div> </c:forEach>

注意${artworkList}能直接用,是因为 Servlet 里setAttribute的 key 是artworkList。许多课程设计源码会在 Jsp 里直接写<%=request.getAttribute("artworkList")%>再手动强转,那种写法也能跑,但页面里混进 Java 代码后维护成本会上升。如果你拿到手的源码到处是 scriptlet,我建议把列表展示逻辑改成 JSTL 的forEach,这是改造过程里性价比最高的一步。

2.3 为什么 SQL 要放在 DAO 层而不是 Servlet 里

有些非科班写的Servlet项目,会直接把 JDBC 代码写进 Servlet 的doPost里,建连、写 SQL、处理结果集、关闭资源全部堆在一个方法中。这种写法在小项目里能跑,但你要新增一个“按作者查询作品”的接口,就得把整段 JDBC 复制一遍;要是某天数据库密码改了,你需要在十几个 Servlet 里找DriverManager.getConnection。分层之后,数据库的连接和释放被收敛到 DAO 层,其他层永远只面向接口编程。

public class ArtworkDaoImpl implements ArtworkDao { @Override public List<Artwork> findByCategoryId(String categoryId) { String sql = "SELECT id, title, author, cover_path, create_time FROM artwork " + "WHERE category_id = ? AND status = 1 ORDER BY create_time DESC"; List<Artwork> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, categoryId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Artwork a = new Artwork(); a.setId(rs.getInt("id")); a.setTitle(rs.getString("title")); a.setAuthor(rs.getString("author")); a.setCoverPath(rs.getString("cover_path")); a.setCreateTime(rs.getTimestamp("create_time")); list.add(a); } } } catch (SQLException e) { throw new RuntimeException("查询作品列表失败", e); } return list; } }

这段代码演示了两件重要的事。第一,用PreparedStatement而不是字符串拼接 SQL,categoryId以参数形式传入,既避免 SQL 注入,又不用手工处理单引号转义。第二,Java 7 的 try-with-resources 语法保证Connection、PreparedStatement、ResultSet都会自动关闭。如果你在源码里看到的是裸conn.createStatement()加 finally 里一堆if (rs != null) rs.close();,那说明代码还停留在 Java 6 的写法,功能没问题,但连接泄漏风险高。

这个项目里还有一个容易被忽略的组件是 Filter。编码过滤器要放在所有 Servlet 之前执行,因为请求参数能不能被正确解码,直接决定后续业务层拿到的是不是乱码。登录校验过滤器也要放在这一层,如果session里没有管理员信息,直接重定向到登录页,而不必在每个 Servlet 里重复写判断。一个好用的展览系统,源码里应该能同时找到这两个 Filter,它们不承担业务逻辑,但决定了整个系统的健壮性。

3. 本地跑通最小闭环:IDEA+Tomcat+MySQL 的配置顺序与关键参数

3.1 版本组合怎么选,才不会被老代码绊倒

标题里没有写明具体版本,这类毕设源码最常见的搭配是 JDK 1.8 + Tomcat 8.5/9.0 + MySQL 5.7,也有不少新一点的工程直接在 MySQL 8.0 上写。我一般这么选:JDK 用 1.8,兼容性最好,@WebServlet注解、lambda 表达式都能用;Tomcat 用 9.0,它对应 Servlet 4.0,跑 Servlet 3.1 的老代码完全没问题;MySQL 用 5.7 比较省心,如果电脑里已经装了 8.0 也没必要卸载,只要在 JDBC URL 里多带两个参数,详见 5.2。

“vscode编写servlet代码需要怎么配置环境”这类问题常见,但我的建议是:这种带 webapp 目录结构的传统工程,直接用 IDEA 打开最省事。VSCode 需要手动装 Java Extension Pack,再配 Tomcat Server 插件,构建路径上稍有不一致就会在启动时报 ClassNotFound;IDEA 的 Community 版就能建 Jsp 项目,Ultimate 版对 Tomcat 集成更顺滑。“idea新建jsp项目”时记得选 Java Enterprise 的 Web Application 模板,然后把源码里的 src 和 web 目录分别标记成 Sources Root 和 Web Resource Directory。

3.2 先建库建用户,连接参数才不会踩雷

拿到 sql 脚本后,我建议不要直接双击用文本编辑器打开再复制到命令行,而是在命令行或 Navicat 里先建一个专用数据库和专用账号,避免用 root 跑应用。命令是通用的:

CREATE DATABASE artwork_exhibition DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 创建应用账号,密码按项目要求改 CREATE USER 'art_admin'@'localhost' IDENTIFIED BY 'Art@2024'; -- 只给这一个库的权限,不要给全局权限 GRANT ALL PRIVILEGES ON artwork_exhibition.* TO 'art_admin'@'localhost'; FLUSH PRIVILEGES;

库名用了utf8mb4,而不是老脚本里常见的utf8。原因很简单:utf8在 MySQL 里最多存 3 个字节,遇到生僻字或 Emoji 就可能落库失败;utf8mb4是它的超集,同样能存中文,行为更可预期。 建完库之后,找到源码里读数据库配置的db.properties或DBUtil.java,把连接参数对齐。最常见的一行配置长这样:

jdbc.url=jdbc:mysql://localhost:3306/artwork_exhibition?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=art_admin jdbc.password=Art@2024

这些参数不是随便写的。useUnicode=true&characterEncoding=utf8保证中文以 UTF-8 编码传输,不配的话 Service 层拿到请求参数就开始乱码;serverTimezone=Asia/Shanghai解决 MySQL 服务器时区与本地不一致导致的 8 小时偏差,常见报错是The server time zone value 'Öйú±ê׼ʱ¼ä';useSSL=false是为了避免 MySQL 8 默认开启 SSL 后本地环境握手失败,这就是很多人遇到的“mysql ssl连接错误”;allowPublicKeyRetrieval=true配合 MySQL 8 的caching_sha2_password认证插件使用,不加会在连接阶段直接抛Public Key Retrieval is not allowed。

3.3 部署到 Tomcat:war 和 war exploded 有什么区别

在 IDEA 里配置 Tomcat 部署时,你会看到两种 Artifact 类型:war和war exploded。开发调试阶段我强烈建议选war exploded,它把项目按目录结构直接展开,改一个 Jsp 文件,刷新页面就能看到效果,不需要重新打包;war是把整个 webapp 压缩成一个包,适合最后交付,也就是“传统jsp项目打包war”这个操作对应的工作流。

如果临时电脑上没有 IDEA,命令行部署也很直接,把打好的 war 包丢进 Tomcat 的 webapps 目录即可:

cp artwork_exhibition.war $CATALINA_HOME/webapps/ cd $CATALINA_HOME/bin ./startup.sh # 启动后访问 http://localhost:8080/artwork_exhibition/

Tomcat 会自动解压 war 包。用这种方式部署,要注意两点:一是解压目录名就是 context path,访问时要带项目名;二是如果重新部署前没有先删掉旧目录,可能出现新旧 class 混用的问题。 如果你在服务器上有 Nginx,要记住“nginx支持jsp吗”的答案是否定的:Nginx 只处理静态资源,Jsp 请求必须反向代理给 Tomcat,配置里写proxy_pass http://127.0.0.1:8080;即可。

端口冲突是另一个高频问题。8080 被其他进程占用时,Tomcat 启动日志会报Port already in use,改conf/server.xml里的 Connector:

<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

改完端口后,数据库、前端跨域等联调参数都要跟着变,所以我不建议轻易换端口,优先找到占用 8080 的进程处理掉。

3.4 连接池参数:不要只看见 maxActive

很多源码里所谓“连接池”其实只是个DriverManager.getConnection的简单封装,每次请求新建连接、用完关闭。这种写法在并发测试下一压就出Too many connections。常见做法是引入 DBCP、C3P0 或 Druid,其中 Druid 因为自带监控页面,在课程设计和中小型项目里见到得最多。一个能落地的连接池配置长这样:

# 初始化连接数:项目启动时预先建好 5 个连接 initialSize=5 # 最大活动连接数:超过这个数,新请求需要等待 maxActive=20 # 最大空闲连接数:低于这个数,池子会补建连接 minIdle=5 # 获取连接的超时时间,单位毫秒,60000 表示 60 秒拿不到连接就报错 maxWait=60000 # 验证连接是否有效的 SQL validationQuery=SELECT 1 # 借出连接超过 180 秒未归还,强制回收 removeAbandonedTimeout=180 removeAbandoned=true

参数表里最容易被忽略的是validationQuery。MySQL 连接空闲超过 8 小时会被服务端断开,连接池里如果还保存着这些“死连接”,下一次借出时执行 SQL 就会报Communications link failure。加了validationQuery=SELECT 1之后,连接借出前会被验证,失效连接自动丢弃。removeAbandoned则是防呆设计:如果代码里某个finally块漏写了conn.close(),Druid 会在 180 秒后强制回收这个连接,避免连接池耗尽。日志里看到removeAbandoned相关的 WARN,先别急着屏蔽,那大概率是代码里有连接泄漏,多查PreparedStatement是否在 try-with-resources 里关闭了。

4. 数据库设计与初始化:从四张核心表到图片存储的边界

4.1 核心表结构与建表 SQL

源码里如果只有一个artwork_exhibition.sql文件,那我认为这是一份合格交付;如果连 SQL 脚本都没有,只让你用 Navicat 手工建表,那这个源码的完整度要打折扣。对于艺术作品展览系统,最核心的四张表是管理员表、分类表、作品表、轮播图表。建表语句我按常见做法直接给出来:

-- 管理员表 CREATE TABLE sys_admin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT '存 MD5 后的密文', real_name VARCHAR(50) DEFAULT '' COMMENT '姓名', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表'; -- 作品分类表 CREATE TABLE art_category ( id INT PRIMARY KEY AUTO_INCREMENT, cate_name VARCHAR(50) NOT NULL COMMENT '分类名,如油画/水墨/摄影', sort_order INT DEFAULT 0 COMMENT '排序权重,数字越小越靠前' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品分类表'; -- 作品表 CREATE TABLE artwork ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT '作品标题', author VARCHAR(50) NOT NULL DEFAULT '' COMMENT '作者', category_id INT NOT NULL COMMENT '所属分类', cover_path VARCHAR(255) DEFAULT '' COMMENT '封面图路径,存相对路径', detail_pics VARCHAR(1000) DEFAULT '' COMMENT '详情图路径,多个用分号分隔', description TEXT COMMENT '作品描述', status TINYINT DEFAULT 1 COMMENT '1 上架,0 下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间', CONSTRAINT fk_art_category FOREIGN KEY (category_id) REFERENCES art_category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品表'; -- 轮播图表 CREATE TABLE sys_banner ( id INT PRIMARY KEY AUTO_INCREMENT, banner_name VARCHAR(50) DEFAULT '', image_path VARCHAR(255) NOT NULL, link_url VARCHAR(255) DEFAULT '' COMMENT '点击后跳转的地址', sort_order INT DEFAULT 0 COMMENT '排序,mysql设置默认值为0 的典型场景', status TINYINT DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页轮播图';

建表时有几个细节值得按同样标准检查。第一,凡是会作为查询条件的字段,比如username、category_id、status,都要在表结构里体现索引;主键用INT AUTO_INCREMENT简单直接,除非将来数据量过亿,否则不需要 UUID 主键。第二,状态字段用TINYINT而不是字符串'上架'/'下架',既省空间又能加索引,页面显示时再通过标签或三元表达式转成文字。第三,文本字段给TEXT,路径字段给VARCHAR(255),不要把图片路径设计成TEXT,因为索引长度有限制。

4.2 时间字段的取舍:DATETIME 还是 TIMESTAMP

艺术作品表里的create_time用DATETIME还是TIMESTAMP,不仔细想会埋一个隐性坑。TIMESTAMP的范围只到 2038 年,而且它在驱动层会按照当前时区做转换;DATETIME范围大得多,也不会自动时区转换。对展览系统这种长期要展示“录入时间”的业务,我一般选DATETIME,配合连接 URL 里的serverTimezone=Asia/Shanghai,时间逻辑最直观。

真正容易翻车的是 Jsp 页面的时间格式化。如果实体类里createTime用的是java.util.Date,页面可以用标签库格式化:

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <fmt:formatDate value="${artwork.createTime}" pattern="yyyy-MM-dd HH:mm" />

但如果源码里实体类写的是java.time.LocalDateTime,上面这段会直接报错,因为 JSTL 的formatDate只认java.util.Date。两种解决办法:一种是在 DAO 层取数时就转成java.util.Date,保持和页面标签兼容;另一种是前台展示时直接调用artwork.createTime.toString()再截取前 16 个字符,虽然不够优雅,但能最快解决问题。 这种问题在“jsp个人信息展示页面”里也会遇到,凡是页面上要显示一条记录创建时间的,都建议先在 SQL 里验证字段类型,再决定页面怎么写。

4.3 初始化数据的写法:编码、默认值和密码

初始化脚本除了建表,还要把几条必须的数据写进去,否则项目启动后一片空白,登录都进不去。写法上注意三点。第一,脚本开头用SET NAMES utf8mb4;保证命令行导入时中文不乱码;第二,插入语句显式列名,不要写成INSERT INTO sys_admin VALUES (...),因为表结构一旦调整,脚本就作废了;第三,管理员密码存 MD5 而不是明文,初始账号密码要写清楚。一条可复用的基础数据插入长这样:

SET NAMES utf8mb4; -- 初始管理员:账号 admin,密码 admin123 -- 注意入库的是 MD5('admin123') 的密文,不是明文 INSERT INTO sys_admin(username, password, real_name) VALUES ('admin', MD5('admin123'), '系统管理员'); INSERT INTO art_category(cate_name, sort_order) VALUES ('油画', 1), ('水墨', 2), ('摄影', 3); INSERT INTO sys_banner(banner_name, image_path, link_url, sort_order) VALUES ('首页主推', 'upload/banner1.jpg', 'artwork/detail?id=1', 1);

密码这个问题我要多说一句:很多老脚本直接把123456明文写在表里,演示是方便了,但只要拿到这个源码的人看一眼数据库,所有管理员账号就全暴露了。维护这类系统时,最直接的“后悔药”是把初始化数据里的密码改成MD5('你的新密码'),登录页核对时把用户输入也做一次 MD5 再比对。MySQL 里MD5()函数在脚本里可以直接用,但如果源码里已经有登录逻辑在对password做二次加密,那就要先读代码再决定初始化数据怎么写,别让加密变成双重叠加。

另外,用 MySQL Workbench 或 Navicat 导入脚本时,要选择“以 UTF-8 编码运行 SQL”,而不是直接复制粘贴。Windows 命令行默认编码往往是 GBK,中文注释会在导入时报语法错误或变成乱码。

4.4 图片只存路径不存文件:上传目录怎么设计

我刚拿到这类源码时,最怕看到数据库里存的是D:/upload/xxx.jpg这种绝对磁盘路径。项目换一台机器部署,路径就全废了。正确做法是数据库只存相对路径,上传的物理文件放在 webapp 的 upload 目录下,前端页面用项目上下文去拼完整地址。上传 Servlet 的核心写法如下:

// 1. 得到 upload 目录的真实磁盘路径 String realBase = getServletContext().getRealPath("/upload"); File uploadDir = new File(realBase); if (!uploadDir.exists()) { uploadDir.mkdirs(); } // 2. 用 UUID 重命名文件,避免中文文件名和特殊字符问题 String ext = fileName.substring(fileName.lastIndexOf('.')); String newName = UUID.randomUUID().toString().replace("-", "") + ext; File dest = newFile(uploadDir, newName); // 3. 把相对路径写进数据库:upload/新文件名.jpg String dbPath = "upload/" + newName;

数据库存upload/xxx.jpg之后,页面展示时用src="${pageContext.request.contextPath}/upload/xxx.jpg",这样部署在任意一台机器上都不会出现死路径。这里要注意两个边界:第一,getRealPath在开发环境指向 IDEA 的 target 目录,在发布环境指向 Tomcat 解压目录,war 包一旦重新部署,之前手工传进 upload 目录的文件会被清掉,所以交付时要么把图片做成静态资源单独放,要么把 upload 指到CATALINA_HOME/conf里的虚拟目录;第二,不要把图片存进 MySQL 的BLOB字段,列表页一次查出几十张图会让查询和网络带宽同时变慢,对这个体量的项目来说,文件系统加相对路径是最平衡的方案。

5. 避坑清单:中文乱码、SSL连接、旧 class 缓存、Session 失效、图片路径

5.1 中文乱码:三个层面必须同时统一

现象:数据库里的中文正常,但 Servlet 往页面输出中文变成???,或者表单提交后入库的中文变成乱码;严重的时候整个页面出现一堆问号,刷新也没用。

原因:乱码很少只有一个来源。Jsp 页面本身的编码、Servlet 读取请求参数时的编码、连接 MySQL 时 URL 里的编码、数据库表默认字符集这四个环,任何一环断掉都会乱码。最常见的组合是 Jsp 写了UTF-8,但 Servlet 里没有request.setCharacterEncoding("UTF-8"),于是 POST 提交的中文被当成 ISO-8859-1 解析,到 Service 层就已经是错的。

解决:用一个编码过滤器统一处理,在 web.xml 里把它配置到所有请求之前:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>com.xxx.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

Filter 内部就三行:request.setCharacterEncoding(encoding)、response.setCharacterEncoding(encoding)、response.setContentType("text/html;charset=" + encoding)。同时把 Jsp 页头写成pageEncoding="UTF-8",数据库表统一用utf8mb4,JDBC URL 带上characterEncoding=utf8。排查乱码时按这个顺序检查,哪个环节缺了补哪个。记住一个原则:三个层面全部统一成 UTF-8,不要留一个“例外”。

5.2 MySQL 8 驱动报 Public Key Retrieval is not allowed

现象:代码在 MySQL 5.7 上跑得好好的,换到 MySQL 8.0 之后,启动项目或第一次执行查询就抛Public Key Retrieval is not allowed,连接直接失败。

原因:MySQL 8.0 默认认证插件是caching_sha2_password,客户端第一次连接时需要从服务器端获取 RSA 公钥来加密密码,但 JDBC 驱动出于安全考虑默认不允许自动获取公钥,于是连接阶段就失败。

解决:两种方式任选。第一种,在 JDBC URL 末尾追加参数:

jdbc:mysql://localhost:3306/artwork_exhibition?useSSL=false&allowPublicKeyRetrieval=true

第二种,建用户时指定mysql_native_password插件:

CREATE USER 'art_admin'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Art@2024';

个人建议用第二种,因为allowPublicKeyRetrieval=true在公网环境下会带来中间人攻击的隐患,本地开发无所谓,但如果你要把项目部署到有公网地址的服务器上,更稳妥的是指定旧版插件。还要顺带检查 mysql-connector-java 的版本,MySQL 8 对应的驱动版本要在 8.x 以上,老驱动对新的认证协议支持不完整,报错现象是Unable to load authentication plugin 'caching_sha2_password'。

5.3 改了 Java 代码,页面却还是旧行为

现象:修改了 Servlet 里的逻辑,重新编译,IDEA 也显示“Build success”,但刷新页面看到的效果和改动前一模一样,甚至删除某个方法后页面依然正常调用,仿佛改动没生效。

原因:这个话题属于典型的“改代码不生效”翻车现场。大多数情况是 IDEA 的 Artifact 配置成了war而不是war exploded,每次构建只是把旧的 class 重新打包,页面对应的目录没有同步;另一部分情况是 Tomcat 的work/Catalina/localhost目录里缓存了旧 class 和解压后的资源,Redeploy 只是把新 class 覆盖上去,缓存的旧版本还在。

解决:按顺序排查。先到项目的target/classes目录确认新的.class文件时间戳是否更新了;再打开 Tomcat 的work/Catalina/localhost下对应项目目录,手动删掉临时文件;最后在 IDEA 的 Run Configuration 里把 On Update Action 改成Redeploy,而不是Restart Server。真正稳妥的做法是:Stop 服务,Remove Artifact,重新 Deploy,再 Start。这套流程看起来笨,但能把这类玄学问题一次清干净。 不要迷信 IDE 右下角的“Build completed successfully”,那个绿条只代表编译通过,不代表部署目录已经同步。

5.4 登录状态失效:跳转回首页又要重新登录

现象:管理员登录成功后,页面跳转到后台首页,但点任意一个功能又跳回登录页;刷新一下,session 里的管理员信息也消失了。

原因:和“jsp个人信息展示页面”里常见的用户登录问题同源。首先是跳转方式问题,登录成功后如果用sendRedirect跳转到后台页面,浏览器会发起一次新的 HTTP 请求,而之前 session 里存的数据还在,正常来说没有问题;真正的问题往往出在登录状态没有写入 session,或者登录过滤器的拦截路径配置错误,把所有请求都拦下来要求登录。另一个经常被忽视的原因是同一浏览器同时访问了两个不同项目名的 webapp,Cookie 里保存了多个 JSESSIONID,路径匹配混乱,导致 session 串了或者被覆盖。

解决:登录成功后的写法应该是在 session 里放对象后,再用重定向去后台首页,注意不要使用forward,因为重定向会重新发起请求,用户能刷新而不重复提交表单:

if ("admin".equals(username) && MD5Util.md5(password).equals(user.getPassword())) { // 登录成功,把用户对象放入 session 域 req.getSession().setAttribute("admin", user); resp.sendRedirect("admin/index.jsp"); } else { // 失败时转发回登录页,并携带错误信息 req.setAttribute("errorMsg", "账号或密码错误"); req.getRequestDispatcher("admin/login.jsp").forward(req, resp); }

登录过滤器拦截时,要放行登录页和静态资源,只拦截/admin/*,并在 session 里取不到admin时重定向到login.jsp。另外检查web.xml里的 session 超时时间,单位是分钟:

<session-config> <session-timeout>30</session-timeout> </session-config>

如果以上都正常,最后去浏览器开发者工具里看 Cookie 的 Path 是不是/,如果 Path 指向了一个具体的项目子路径,换一个 URL 访问时 Cookie 就带不过去。

5.5 列表页图片能显示,详情页图片红叉

现象:作品列表页的缩略图正常显示,点进详情页后,图片区域是红叉或者空白。前台换一台电脑访问,列表页的图也不出来了。

原因:最典型的两个。一是数据库里存的是绝对磁盘路径,比如D:\workspace\upload\1.jpg,列表页碰巧能显示是因为图片请求在同一次会话里复用了某种缓存,换一台机器后这个路径在服务器上根本不存在;二是详情页拼接路径时用了错误的相对路径,artwork_detail.jsp位于/artwork/detail这种二级路径下,upload/xxx.jpg会被浏览器解析成/artwork/upload/xxx.jpg,自然 404。

解决:统一在页面里用${pageContext.request.contextPath}拼接图片路径:

<img src="${pageContext.request.contextPath}/${artwork.coverPath}" alt="${artwork.title}" />

同时保证数据库里存的只有upload/xxx.jpg这一段相对路径,不出现盘符、不以/开头。如果是生产环境需要把图片放到 Tomcat 之外,可以在server.xml的 Host 节点下配置虚拟目录:

<Context docBase="/data/artwork_upload" path="/upload" reloadable="false" />

这样数据库里的upload/xxx.jpg会被映射到/data/artwork_upload/xxx.jpg,war 重新部署也不会丢文件。最后检查一下upload目录的读写权限,Linux 服务器上如果 Tomcat 进程对 upload 目录没有写权限,上传时会报FileNotFoundException,这个报错信息不会提示“没有权限”,排查起来最容易绕弯路。

6. 进阶:按业务主流程做一轮验收,再决定这个项目值不值得往下加

拿到源码并部署成功之后,先不要急着改业务,而是按主流程把系统从登录到展示完整走一遍。下面是我常用的验收清单,适合任何基于 Servlet+Jsp+MySQL 的展览系统:

验证项操作步骤预期结果
管理员登录用初始化脚本里的 admin 账号登录跳转后台,不再弹回登录页
分类维护后台新增“书法”分类前台首页分类下拉框能选到它
作品上传提交标题、作者、分类、封面图列表页出现新作品且图片可打开
前台浏览匿名访问首页作品正常展示,分类切换和详情页无 500
下架作品后台把作品状态改为 0前台不再展示,数据库记录仍保留

跑清单时顺手执行几个 SQL,能快速判断数据层是否健康:

-- 前台可见作品数量 SELECT COUNT(*) FROM artwork WHERE status = 1; -- 关联查询:作品标题和它所属的分类名 SELECT a.id, a.title, c.cate_name FROM artwork a LEFT JOIN art_category c ON a.category_id = c.id; -- 查看当前数据库连接情况,排查连接池是否被耗尽 SHOW FULL PROCESSLIST;

如果这个清单全部通过,再想加功能,我建议先加“作品推荐”而不是直接上复杂的统计报表。给artwork表加一个is_recommend TINYINT DEFAULT 0字段,后台多一个勾选框,前台首页按is_recommend = 1查询即可。这个改动会同时涉及数据库、DAO、Servlet、Jsp 四个层面,正好把前面理解的分层再完整练一遍。 至于要不要从这套老栈迁移到 Spring Boot,我的判断是:只要项目还在稳定运行,就没有必要为了“新”而重写;但如果你打算以这个项目为基础继续找 JavaWeb 开发工作,那可以对照这份源码,把 Controller、Service、Mapper 三层的对应关系在 Spring Boot 里重新实现一遍,两相对照,收获会很大。

我现在拿到这类老项目,已经养成了一个习惯:先按清单跑主流程,再动代码。以前图快,直接改 Jsp 界面,最后发现一个查询参数问题要翻三层代码才查明白,反而更慢。希望帮到你。

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

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

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

立即咨询