简介Java动漫之家系统设计与实现是一套面向动漫爱好者在线互动平台的完整开发设计方案适用于JavaWeb课程设计、毕业设计或快速搭建动漫资源社区的项目预研。该方案以SSM框架为核心结合MySQL数据存储与HTML5前端交互从系统背景、可行性分析、技术选型、功能设计、开发流程到测试与评估均有详细论述并覆盖用户、动漫资源、新闻资讯、论坛互动和管理员后台等典型模块。资源压缩包仅包含1个docx文档大小6.68MB无需额外配置即可直接阅读。对于正在撰写同类课题或需要快速理解动漫社区系统实现的读者文档内容能帮助厘清SSM项目的分层架构、数据库设计思路以及完整论文结构的组织方式节省从零梳理和撰写的时间。当前已有174人学习可作为课程设计或毕业设计阶段的实用范本。1. 动漫之家系统是什么一个能跑通全流程的 Java Web 练手项目如果你见过基于java动漫之家系统设计与实现.docx这类标题大概率是在毕业设计选题库、课程设计任务书或者二手代码交易平台里刷到的。它说白了就是一个很典型的 Java Web 信息管理系统前台面向访客展示动漫资讯、分类浏览作品后台给管理员做内容录入、上下架、用户审核和评论管理。作为练手项目它的价值在于麻雀虽小五脏俱全——登录鉴权、CRUD、分页、文件上传、权限控制这些面试简历上常写的点它都占齐了。这类系统最适合两类人一是正在做毕业设计、需要短时间跑通一个完整项目的在校生二是刚学完 Java 语法、想搞清楚 Servlet 和数据库到底怎么串起来的自学者。我见过不少同学拿这份标题去搜源码结果下到一堆报错拼不起来的半成品或者被过时的 JDBC 写法坑到崩溃。这篇笔记不讲网上那些抄来抄去的空洞需求分析直接按设计→编码→排错→扩展的思路把这份标题背后你要交付的东西一次性讲透。2. 系统设计先行需求拆解、数据表设计与核心关联关系2.1 需求范围怎么定从交付口径看功能边界做管理系统最忌讳一上来就写代码。我见过某开发者拿到动漫之家这个标题后先花三天画了十张表结果答辩时被评委问为什么用户表里会有头像上传和个性签名你前面需求里根本没有提到现场改口都来不及。正确的做法是先划定两条线必须有的核心功能和可以用来加分的扩展功能。核心功能一般跑不掉这几块用户模块注册、登录、修改资料、退出登录、动漫信息模块新增、编辑、删除、列表分页、关键词搜索、分类模块按类型浏览比如热血、恋爱、科幻、评论模块登录用户对单部动漫发表评论。扩展功能常见选项有收藏功能、轮播图管理、数据统计、评论审核。如果你是拿去做毕业设计我建议把核心功能做完做稳扩展功能挑一个做得深一点比如收藏功能做到收藏列表取消收藏收藏数统计这比堆五个半成品功能更有答辩说服力。2.2 数据表设计用户、动漫、分类、评论的字段与约束如果你用文档里的 .docx 标题方向做设计最终交付物里数据库设计章节一般要求画出 E-R 图和建表 SQL。这里的套路很固定四张主表加若干张关联表。下面先给你四张核心表的建表 SQL这是整个系统最基础也是后面最容易改出问题的部分。请注意字段类型和字符集的选择我见过太多人栽在 varchar 长度和 utf8mb4 编码上。CREATE DATABASE IF NOT EXISTS anime_home DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE anime_home; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, username VARCHAR(30) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT 密码MD5后可考虑加盐, nickname VARCHAR(30) DEFAULT COMMENT 昵称, avatar VARCHAR(255) DEFAULT COMMENT 头像图片路径, role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称, sort_order INT DEFAULT 0 COMMENT 排序权重越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT动漫分类表; CREATE TABLE anime ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 动漫名称, category_id INT NOT NULL COMMENT 所属分类, cover_image VARCHAR(255) DEFAULT COMMENT 封面图路径, description TEXT COMMENT 简介, status TINYINT DEFAULT 1 COMMENT 1展示 0隐藏, view_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_id (category_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT动漫信息表; CREATE TABLE comment ( id INT AUTO_INCREMENT PRIMARY KEY, anime_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL COMMENT 评论内容, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_anime_id (anime_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;这段建表 SQL 里有两个细节容易被忽略。一是所有文本字段用 utf8mb4 而不是 utf8因为动漫简介、评论内容里很可能出现 emoji 表情utf8 根本存不进 4 字节的符号到时候报错你查半天都找不到原因。二是 anime 表里的 update_time 用了 ON UPDATE CURRENT_TIMESTAMP这是 MySQL 5.6 以上版本常用的做法能在每次 UPDATE 时自动刷新时间省去在 Java 代码里手动 set 的麻烦。字段注释一定要写全文档里截图和答辩 PPT 都要用这些注释。2.3 表关联关系外键、索引与常用查询路径四张表的关系其实非常直接一个分类下有多部动漫一对多一部动漫下有多条评论一对多一个用户能发多条评论一对多。我建议在实际建表时不加数据库外键约束只保留逻辑关联和索引理由后面避坑章节会细讲。但设计文档里必须画清楚 E-R 图这里给你文字版描述user 和 comment 通过 user_id 关联anime 和 comment 通过 anime_id 关联category 和 anime 通过 category_id 关联。查询路径上最常用的是两段 SQL首页按分类刷动漫列表以及详情页查动漫信息和评论列表。前者是 join 还是子查询我推荐直接用 join 一次查出分类名称避免 N1 问题。SELECT a.id, a.title, a.cover_image, a.description, a.view_count, c.name AS category_name FROM anime a LEFT JOIN category c ON a.category_id c.id WHERE a.status 1 ORDER BY a.create_time DESC LIMIT 0, 12;这里 LIMIT 0, 12 是 MySQL 的分页写法第一个数字是偏移量第二个是每页条数。你可以把它包装成通用分页工具后面第 4 章会给出对应 Java 参数。LEFT JOIN 在这里很安全因为 anime 表里的 category_id 在业务上不允许为空LEFT 和 INNER 结果一致顺手兼容了脏数据。3. 技术选型与工程结构为什么推荐经典 JSPServlet 而不是一上来就 Spring Boot3.1 技术栈对比JSPServlet、Spring MVC、Spring Boot 各自适合什么你搜标题里的基于java下载下来的项目大概率分成三派老派用 JSPServlet中派用 Spring MVCXML 配置新派直接用 Spring Boot。到底选哪个我的建议很实际如果你是拿来做毕业设计或课程设计优先选 JSPServlet除非你的任务书里明确写了 Spring 系列。原因有两点第一答辩时老师大概率会往框架深处问JSPServlet 的逻辑透明每一行代码你都能解释清楚Spring Boot 里的自动配置一问你就容易露怯第二JSPServlet 项目部署到 Tomcat 直接扔 webapps 就能跑不涉及 Maven 依赖冲突等一堆环境问题这个选题最怕的就是最后打包起不来服务。Spring Boot 就像动漫之家里的豪华版手办好看但维护成本高。等你在 JSPServlet 上把登录、分页、上传全套走通一遍再去学 Spring Boot 就很容易理解了那时你会发现 Spring Boot 只是帮你省掉了重复的样板代码业务逻辑本身没变。3.2 分层结构entity、dao、service、servlet、util 的责任划分工程结构推荐用最经典的包划分别一层 controller 包解决战斗。我习惯这样建包src/ ├─ com/animehome/ │ ├─ entity/ 数据库表对应的实体类字段名与表列一一对应 │ ├─ dao/ JDBC 或 MyBatis 的数据访问接口和实现 │ ├─ service/ 业务逻辑层处理事务和业务规则 │ ├─ servlet/ 控制器层接收请求、调用 service、跳转或返回 JSON │ ├─ util/ 数据库连接、MD5、分页等工具类 │ └─ filter/ 编码过滤器、登录拦截过滤器 └─ web/ ├─ WEB-INF/web.xml ├─ admin/ 后台管理页面 └─ front/ 前台页面entity 就是纯 POJO字段类型要和数据库严格对应比如 MySQL 的 TINYINT 对应 Java 的 Integer 而不是 boolean因为数据库里可能存 0/1/2 这类多状态值。dao 层只做增删改查service 层处理删除动漫时同时删除它的评论这类跨表事务servlet 层只做参数接收、调用、重定向。我见过大量网上下载的代码把 SQL 直接写在 servlet 里后面想加个权限判断就是一场大改所以分层这个习惯早点养好后面改功能会舒服得多。3.3 环境搭建与最小可运行骨架JDK、Tomcat、数据库连接配置环境版本是整个项目能不能跑起来的关键。我建议 JDK 用 8 或者 11Tomcat 用 8.5 或 9MySQL 用 5.7 或 8.0。别图新鲜用 JDK 17 配老 Tomcat坑多到你想砸电脑。数据库连接用 JDBC 还是连接池这一步很多人一开始图省事直接 DriverManager 裸连结果上线前被老师问到你的数据库连接能支持几个人并发就哑火了。所以我建议哪怕是练手项目也用连接池常见做法是引入 commons-dbcp2 或 druid下面给一个最小连接池配置。Context Resource namejdbc/animehome authContainer typejavax.sql.DataSource maxTotal100 maxIdle30 maxWaitMillis10000 usernameroot passwordyour_password driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/anime_home?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ /Context这段是放在 Tomcat 的 context.xml 或项目 META-INF/context.xml 里的数据源配置。maxTotal100 表示连接池最大连接数maxIdle30 是空闲连接数maxWaitMillis10000 是拿到连接的超时时间超过 10 秒直接抛异常避免线程无限等待。url 里的 useUnicode 和 characterEncoding 是必须带的不然中文乱码问题会在第 5 章等着你。password 记得换成你自己的不要用 root 弱口令到处跑。连接池在 Java 侧怎么取我习惯写一个工具类public class DBUtil { public static Connection getConnection() { try { Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:/comp/env/jdbc/animehome); return ds.getConnection(); } catch (Exception e) { throw new RuntimeException(数据库连接获取失败 e.getMessage()); } } }这里用的 JNDI 方式从 Tomcat 容器里拿数据源好处是密码配置在 xml 里代码里没有明文。注意 lookup 的前缀必须是 java:/comp/env/后面跟你在 context.xml 里定义的 Resource name写错会直接抛 NameNotFoundException。拿到 Connection 之后的所有操作和裸 JDBC 一样但必须记得在 finally 里关闭连接后面避坑章节会解释连接池泄漏是怎么回事。4. 核心功能实现从登录鉴权到动漫信息管理的可复现代码4.1 数据库连接与工具类避免连接泄漏的基础写法前面给了 DBUtil 的获取连接方法这里补一个完整的 BaseDao 模板这是所有 DAO 类的地基。很多网上代码把 getConnection 写在每个方法里逻辑是通的但重复代码多到人崩溃。public class BaseDao { protected Connection conn; protected PreparedStatement ps; protected ResultSet rs; protected void closeAll() { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } protected int executeUpdate(String sql, Object... params) { int rows 0; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rows ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return rows; } }executeUpdate 用可变参数接收 SQL 里的问号占位符params.length 就是占位符个数setObject 从下标 1 开始。这里有几个强制约定SQL 字符串里不允许拼接参数一律用 ? 占位closeAll 的顺序一定是 rs、ps、conn 从内到外finally 保证任何异常条件下都会释放资源。如果你发现某个页面数据库连接数一直涨八成是操作完没关连接或者提前 return 跳过了 finally这种问题排查思路在第 5 章会展开。4.2 登录与 session 管理Filter 拦截与密码处理登录是每个管理系统第一个要做的功能。密码存储千万别用明文至少做一次 MD5。虽然 MD5 现在能被彩虹表撞破但你做课程设计可以接受只要在答辩时说生产环境会改用 BCrypt就能过关。下面这段代码是登录验证最稳的逻辑块。public User login(String username, String password) { String md5Pwd MD5Util.md5(password); String sql SELECT * FROM user WHERE username? AND password? AND status1; User user null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, md5Pwd); rs ps.executeQuery(); if (rs.next()) { user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setNickname(rs.getString(nickname)); user.setRole(rs.getInt(role)); } } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return user; }登录成功后在 Servlet 里执行 session.setAttribute(loginUser, user)然后重定向到首页失败则回显用户名或密码错误。status1 这个条件容易漏掉作用是禁用账号直接不允许登录这是后台审核账号的必要条件。角色 role 字段在 LoginServlet 中拿来做跳转判断管理员跳 admin/index.jsp普通用户跳 front/index.jsp这一步能做得更优雅但最简单可靠的还是 if-else。拦截未登录请求时我用 Filter 实现权限控制WebFilter(/admin/*) public class AdminAuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); User loginUser (session null) ? null : (User) session.getAttribute(loginUser); if (loginUser ! null new Integer(1).equals(loginUser.getRole())) { chain.doFilter(request, response); } else { response.sendRedirect(request.getContextPath() /user/login.jsp); } } }这里 WebFilter 注解是 Servlet 3.0 提供的不用在 web.xml 里配 filter 映射。请求 admin 目录下的所有页面都会被这个 filter 拦住先看 session 里有没有 loginUser再看角色是不是 1。有一个细节很多新手踩坑request.getSession(false) 里参数传 false 表示如果当前 request 没有关联 session 就返回 null不要自动创建 session否则攻击者访问每个静态资源都会产生一次 session服务器内存白涨。如果需要登录后跳回原页面可以在 LoginServlet 里记录 request.getHeader(Referer)但这是锦上添花的点不做也不影响答辩。4.3 动漫信息增删改查分页、文件上传与参数校验后台管理的核心是动漫信息的增删改查这里分页和文件上传两块最容易翻车。先看分页查询的 Service 层实现。public PageResultAnime listAnime(int page, int pageSize, String keyword, Integer categoryId) { StringBuilder sql new StringBuilder(SELECT a.*, c.name AS category_name FROM anime a LEFT JOIN category c ON a.category_idc.id WHERE 11 ); StringBuilder countSql new StringBuilder(SELECT COUNT(*) FROM anime a WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND a.title LIKE ? ); countSql.append(AND a.title LIKE ? ); params.add(% keyword.trim() %); } if (categoryId ! null) { sql.append(AND a.category_id? ); countSql.append(AND a.category_id? ); params.add(categoryId); } sql.append(ORDER BY a.create_time DESC LIMIT ?, ?); params.add((page - 1) * pageSize); params.add(pageSize); // 执行 countSql 得到总记录数 total执行 sql 得到当前页列表 list // 返回 PageResultAnime包含 total、page、pageSize、list 四个字段 }WHERE 11 是老派动态 SQL 的写法看起来有点丑但确实能省掉判断是否已有 where 条件的逻辑。注意 countSql 不能和主查询共用 ORDER BY 和 LIMIT否则统计总数会不正确。分页参数 page 从 1 开始Java 计算偏移量要写成 (page - 1) * pageSizeSQL 里的 LIMIT 第一个参数就是偏移量第二个是 pageSize。比如第 3 页每页 12 条偏移量就是 24。文件上传在 Servlet 里的标准做法是用 multipart 配置MultipartConfig(maxFileSize 1024 * 1024 * 2, maxRequestSize 1024 * 1024 * 10) WebServlet(/admin/anime/add) public class AnimeAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); Part coverPart request.getPart(coverImage); String coverPath ; if (coverPart ! null coverPart.getSize() 0) { String fileName UUID.randomUUID().toString().replace(-, ) _ coverPart.getSubmittedFileName(); String uploadDir getServletContext().getRealPath(/uploads); File dir new File(uploadDir); if (!dir.exists()) dir.mkdirs(); coverPart.write(uploadDir File.separator fileName); coverPath uploads/ fileName; } // 从 request 获取 title, categoryId, description 并调用 service 保存 } }maxFileSize 限制单张图片 2MBmaxRequestSize 限制整个请求 10MB这是防攻击的基本底线。文件名用 UUID 拼上原始文件名避免两个人同时传同名文件互相覆盖。getServletContext().getRealPath(/uploads) 拿到的是你在 IDE 里部署目录的绝对路径上传时要用这个真实路径写文件但存到数据库里的路径必须是相对路径uploads/xxx.jpg因为页面渲染时要通过浏览器访问 URL浏览器不关心你的磁盘绝对路径。这里容易踩的坑我在第 5 章专门讲。5. 避坑指南动漫之家开发中我踩过的 5 个典型问题5.1 中文字符乱码的三种来源与一次性解法现象页面上全是????或者叫我数据库里是正常的后台管理页面却显示乱码。原因乱码本质是写入数据的编码和读取数据的编码不一致。三个常见来源一是 MySQL 表不是 utf8mb4二是在 JDBC 连接字符串里没写 characterEncodingutf8三是 Servlet 没有设置 request.setCharacterEncoding(UTF-8)。Post 请求的编码需要在读取参数前设置Get 请求的编码则依赖 Tomcat 的 URIEncoding。解决一劳永逸的做法是三处同时设。建表时用 utf8mb4JDBC 连接串带 useUnicodetruecharacterEncodingutf8在 Servlet 的 doPost 开头第一句加 request.setCharacterEncoding(UTF-8)。另外 Tomcat 的 server.xml 里给 Connector 加 URIEncodingUTF-8 能解决 Get 参数乱码但这个配置会影响整个 Tomcat 下的所有项目如果只是为了这一个项目建议直接在 getParameter 前对值做 new String(value.getBytes(ISO-8859-1), UTF-8)因为 Tomcat 8 默认 URIEncoding 已经是 UTF-8 了老代码里的转码套路反而会把正常字符串搞坏。5.2 文件上传后访问 404路径保存与虚拟目录配置现象后台显示上传成功数据库里也有路径但浏览器访问 uploads/xxx.jpg 时 404。原因八成是你把绝对路径存到了数据库比如存成 E:/workspace4/anime_home/web/uploads/xxx.jpg这串路径在本地能用别人部署到 Linux 或者换个目录就全挂了。还有可能是 IDE 里的部署目录不是你写文件的目录Tomcat 运行时 getRealPath 指向的是 target 或 webapps你手动建的 uploads 目录可能不在里面。解决数据库永远只存 URL 相对路径 uploads/xxx.jpg。另外上传目录不一定要放在项目里面更稳妥的做法是放到项目外面然后在 Tomcat 配一个虚拟目录别名。我在实际项目中习惯在 server.xml 的 Host 节点下加 然后把 uploads 文件全写进这个物理目录浏览器访问 /uploads/... 时自动映射过去。这样项目重新部署不会丢用户上传的图片。如果你用 IDE 跑 Tomcat经常遇到上传成功但页面不显示先看 catalina 日志里部署的 docBase 在哪然后用文件管理器确认文件到底写到了哪里。5.3 分页查询参数类型转换引发 SQL 注入隐患现象分页页码传了个非数字字符串页面直接 500控制台报 NumberFormatException。原因Servlet 里用 Integer.parseInt(request.getParameter(page))别人构造特殊请求就能让你服务崩溃。更危险的是有人直接用字符串拼接 SQL那就不只是崩溃而是 SQL 注入。我在某模拟项目 X 里见过网上下的代码直接把 page 参数拼进 LIMIT page , 12这个等于裸奔。解决所有从 request 拿到的参数先过类型转换守卫转不成数字就返回默认值。int page 1; String pageStr request.getParameter(page); try { if (pageStr ! null pageStr.matches(\\d{1,6})) { page Integer.parseInt(pageStr); } } catch (NumberFormatException e) { page 1; }正则 \d{1,6} 限制了必须是 1 到 6 位纯数字负数和超长数字直接落回默认值。SQL 语句里的所有查询条件都走 PreparedStatement 的问号占位符参数通过 setObject 传递这样即使传进来带单引号的内容也只被当作普通文本不会改变 SQL 结构。这是最底层防线一定要坚持。5.4 session 超时与并发登录互相踢下线现象用户开着页面去吃饭回来点按钮就跳回登录页提示没有权限更诡异的是管理员在一台电脑上登录另一台电脑也被顶下线。原因前一个问题是 session 默认超时时间 30 分钟到了服务器销毁了 session访问 admin 被 filter 拦截。后一个问题是登录后只用一个 session 变量存用户而 session 是跟浏览器会话绑定的两个浏览器访问同一个服务器是不同 session ID不该互相影响。但如果你的 LoginServlet 里用了全局静态变量存 loginUser 或者用了 application 范围存用户那就会互相踢。解决把用户信息存成 HttpSession 属性并且只在登录成功那一刻 setAttribute不要使用 ServletContext 来存用户对象。session 超时时间可以在 web.xml 里配置单位是分钟。session-config session-timeout60/session-timeout /session-config为了提升用户体验你还可以在前端加一个操作时每隔 30 分钟请求一次心跳接口来延长 session 存活但这个方案会增加复杂度答辩时提一下思路就够了。如果发现后台明明登录着每过一阵子就掉线先看是不是跨域了比如你通过 127.0.0.1 打开页面页面里却用 localhost 请求接口那 session ID 会因为 cookie 域不同而丢失这类玄学掉线一大半是域名不一致导致的。5.5 数据库连接池耗尽重启才好使的玄学真相现象系统跑半天后所有页面都卡住点一次要转很久最后报 Cannot get connection, pool exhausted重启 Tomcat 又好了。原因连接池里的连接被借出后没有归还而且这是无限累积的。最常见的有三种一是 DAO 里有个别方法用了 try-catch 没写 finally只要执行过程抛异常连接就不 close二是你把 conn 写成了局部变量但方法的 return 写在 try 里finally 被漏了三是某个封装好的方法里嵌套调用了另一个 DAO 方法内层把连接关了外层还在用于是外层在异常分支没关连接堆积。这个坑我当年在一个某跨平台系统的线上环境里查了两天才定位最后发现是日志里每次打印 SQL 的方式锁住了连接。解决规范统一为获取连接的方法必须 try-finally 归还或者用 try-with-resources 语法让 JDK 帮你关闭。try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { /* 组装对象 */ } } } catch (SQLException e) { e.printStackTrace(); }try-with-resources 是 Java 7 引入的凡是实现了 AutoCloseable 的类都能放在 try 括号里代码块结束自动调 close异常时也保证关闭。注意 ResultSet 的关闭顺序它依赖 Statement所以也要放在内层 try 里避免外层 PreparedStatement 关闭后 ResultSet 再尝试关闭引发奇怪异常。修复后建议没有正常关闭就打印一条日志用最笨的方法观察连接池 active count 是否持续增长这个数字稳定不涨就说明问题解决。6. 让系统更像产品验证清单与三个值得扩展的功能点系统能跑了不代表交付完成我每次做完这类项目都会走一遍验收清单。打开前台首页看静态资源是否 404不登录直接访问 admin 目录应该被踢到登录页登录后上传一张超过 2MB 的图应该报错而不是抛 500把评论内容输入换行和 emoji确认数据库能存能读连续关掉浏览器再打开确认登录 session 失效用 SQL 注入字符串搜索动漫标题确认不报错且查不到数据。这六条过完你的系统至少经得起常规演示。扩展点我推荐做三个性价比高的一是热门动漫排行榜只要按 view_count 排序查 top 10前台右侧加个小模块就能让页面显得内容丰富二是登录防暴力破解记录同一用户名连续失败次数超过 5 次锁定 15 分钟这个点讲出来答辩很加分三是导出动漫清单为 Excel用 POI 一行行写入逻辑不复杂但很显工作量。这三个都建议在核心功能稳固后挑一个做别贪多。最后的个人习惯提醒每天改完代码提交前在本地按上面清单过一遍再打包 War 部署到另一个干净的 Tomcat 里跑一次。我当年因为图方便一直在 IDE 里点运行没试过独立部署结果交付前一天发现项目部署到纯净环境后所有上传图片全部丢失因为路径写死了 IDEA 的虚拟目录。这种血泪经验多了你就知道了能跑和能交付之间还差一次换环境复现。希望我这篇笔记能帮你少踩几个坑把动漫之家做扎实换一个环境也能稳妥跑起来。本文还有配套的精品资源点击获取