首页 / 资讯中心 / 文章详情

SSM+JSP+MySQL羽毛球平台实战:从XML配置到emoji存储

SSM+JSP+MySQL羽毛球平台实战:从XML配置到emoji存储 ★ FEATURED ARTICLE
简介这是一套面向计算机专业本科生的Java毕业设计实战项目基于SSMSpringSpringMVCMyBatis框架与JSP前端技术构建专为毕设选题、课程设计及Java进阶学习者提供开箱即用的羽毛球主题社交平台解决方案。资源完整覆盖用户端与管理员端双角色功能用户可完成注册登录、浏览新闻、场地预约与评论、在线留言、个人资料管理管理员可进行用户/场地/新闻/留言等全维度后台管控。压缩包共48.8MB含可直接运行的Java源码、MySQL数据库脚本、详细开发说明文档及配套软件工具文件类型以.java业务逻辑类、.jsp页面模板、.sql建表语句和.docx文档为主结构清晰、注释规范已通过导师验收并完成全流程调试。目前已有132人下载学习适合需要真实项目练手、理解MVC分层架构、掌握前后端交互与数据库设计的初学者与毕设学生。1. 这不是又一个“学生管理系统”SSMJSPMySQL 羽毛球交流平台为什么能跑通、能上线、还能真被球友用你搜“Java毕业设计”满屏是“图书管理系统”“学生成绩管理”“宿舍管理系统”——功能雷同、界面简陋、数据库字段凑数、连登录都卡在 session 失效上。但这个标题里的「羽毛球交流平台」不是贴标签它天然带真实业务约束——球友约场要实时查空闲场地、发帖带图片和地理位置、评论需按热度排序、私信得支持未读标记、用户等级得靠积分动态计算。这些需求倒逼你必须把 SSMSpring SpringMVC MyBatis的事务边界划清楚、JSP 的 EL 表达式和 JSTL 标签用到位、MySQL 的索引和字符集配合理、甚至得处理utf8mb4下 emoji 存储这种“小问题”。这不是练手项目是微型社交产品雏形。适合大三下到大四上、已写过 Servlet/JDBC 小项目、正卡在“怎么把框架串成闭环”的同学——它不教你 Spring Boot 自动装配但让你亲手拧紧每一颗螺丝从web.xml的 DispatcherServlet 注册到mybatis-config.xml里 typeAliases 的路径映射再到 JSP 页面里c:forEach嵌套c:if的渲染逻辑。做完它你简历上写的“熟悉 SSM 框架”才不是空话。2. 从零搭起骨架SSM 三层结构怎么分、为什么这么分、每个配置文件改哪几行2.1 为什么坚持用 XML 配置而非注解全开——这是毕业设计的“可控性刚需”很多教程一上来就SpringBootApplicationRestController看似省事但毕业答辩时老师问“你的事务控制在哪一层生效MyBatis 的 SqlSession 是怎么被 Spring 管理的DispatcherServlet 怎么知道该把/post/list转给哪个 Controller”——你答不上来就暴露了“只会抄代码”。而 XML 配置spring-context.xml、spring-mvc.xml、mybatis-config.xml像一张透明电路图spring-context.xml定义 Service 层 Bean 和事务管理器tx:annotation-driven/必须配transaction-managertransactionManagerspring-mvc.xml只管 Controller 和视图解析器InternalResourceViewResolver的prefix/WEB-INF/jsp/和suffix.jsp不能错mybatis-config.xml专注 SQL 映射typeAliases里packagecom.yumao.entity比typeAlias逐个写更安全。提示别信“XML 过时论”。毕业设计评审看的是你对容器生命周期的理解深度不是你用了多少新语法。2.2web.xml是整个项目的“总开关”漏掉这 3 行就启动失败!-- web.xml -- ?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_4_0.xsd version4.0 !-- 1. 加载 Spring 上下文 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-context.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 2. 配置 DispatcherServlet -- servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping !-- 3. 字符编码过滤器关键否则中文存库乱码 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-app逻辑说明第 1 步ContextLoaderListener加载spring-context.xml创建 IoC 容器Service/Dao 层在此初始化第 2 步DispatcherServlet加载spring-mvc.xml创建 Web 容器Controller 层在此加载load-on-startup1/load-on-startup确保它早于其他 Servlet 启动第 3 步CharacterEncodingFilter必须放在最前url-pattern/*/url-pattern否则 POST 请求中文参数进 Controller 就是??。参数说明contextConfigLocation的classpath:前缀表示从src/main/resources目录找配置文件DispatcherServlet的url-pattern//url-pattern是 RESTful 风格基础但注意它会拦截所有请求包括.jsp所以后续 JSP 页面必须放在/WEB-INF/jsp/下外部无法直接访问由 Controller 跳转forceEncodingtrue强制覆盖 request/response 的编码比只设encodingUTF-8更可靠。2.3 MyBatis 的SqlSessionFactoryBean配置别让数据库连接成为“玄学”!-- spring-context.xml 中 MyBatis 部分 -- bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource destroy-methodclose property namedriverClass valuecom.mysql.cj.jdbc.Driver/ property namejdbcUrl valuejdbc:mysql://localhost:3306/badminton?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrueamp;useSSLfalse/ property nameuser valueroot/ property namepassword value123456/ property nameinitialPoolSize value5/ property namemaxPoolSize value20/ property nameminPoolSize value3/ property nameacquireIncrement value2/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.yumao.dao/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean逻辑说明数据源用c3p0非 HikariCP是因为毕业设计环境简单c3p0 的日志更友好出错时能看到连接池状态JDBC URL 中useUnicodetruecharacterEncodingutf8mb4解决中文和 emoji 存储羽毛球表情 必须支持serverTimezoneAsia/Shanghai避免 MySQL 8.0 的时区报错allowPublicKeyRetrievaltrueuseSSLfalse是本地开发绕过 SSL 认证的务实选择生产环境必须配 SSLMapperScannerConfigurer自动扫描com.yumao.dao包下所有接口无需手动注册 Mapper Bean但要求 DAO 接口名必须与mapper/*.xml文件名一致如PostDao.java对应PostMapper.xml。参数说明initialPoolSize5应用启动时预创建 5 个连接避免首请求慢maxPoolSize20最大连接数设太高易占满 MySQL 连接数默认 151需show variables like max_connections;查mapperLocationsclasspath:mapper/*.xml指定 MyBatis XML 映射文件位置classpath:表示src/main/resources/mapper/目录。3. JSP 页面不是“写完就能用”EL 表达式、JSTL 标签、表单提交的三重校验链3.1 JSP 页面必须声明的 3 个指令少一行就编译失败% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html head meta charsetUTF-8 title羽毛球交流平台 - 首页/title /head body h1欢迎${sessionScope.user.username}/h1 c:if test${not empty sessionScope.user} a href${pageContext.request.contextPath}/logout退出登录/a /c:if /body /html逻辑说明% page contentTypetext/html;charsetUTF-8 %告诉浏览器用 UTF-8 解析 HTML否则中文标题显示为方块% taglib prefixc ... %引入 JSTL Core 标签库c:if、c:forEach才能用% taglib prefixfmt ... %引入格式化标签fmt:formatDate显示时间才不报错${sessionScope.user.username}从 Session 作用域取值比request.getAttribute(user)更安全避免页面跳转时丢失${pageContext.request.contextPath}动态获取项目上下文路径如/badminton避免硬编码/badminton/logout导致部署到 ROOT 时 404。参数说明contentTypetext/html;charsetUTF-8中charsetUTF-8是关键缺了它JSP 编译后 HTML 的meta charsetUTF-8也救不了乱码urihttp://java.sun.com/jsp/jstl/core是 JSTL 1.2 的标准 URITomcat 8 自带无需额外 jarsessionScope明确指定作用域防止同名变量在 page/request/session/application 中冲突。3.2 表单提交的“防重复提交”实战JSP Controller MySQL 三级联动用户点击“发布帖子”按钮网络延迟导致他连点两次后端若没处理就会插入两条一模一样的记录。解决方案不是前端加disabled可绕过而是服务端幂等Step 1JSP 页面加唯一 tokenc:set vartoken value${sessionScope.token} scopepage/ c:if test${empty token} c:set vartoken value%java.util.UUID.randomUUID().toString()% scopesession/ /c:if form action${pageContext.request.contextPath}/post/save methodpost input typehidden nametoken value${token} textarea namecontent/textarea button typesubmit发布/button /formStep 2Controller 校验并消费 tokenController public class PostController { RequestMapping(/post/save) public String savePost(RequestParam String content, RequestParam String token, HttpSession session, Model model) { // 1. 校验 token 是否存在且匹配 String sessionToken (String) session.getAttribute(token); if (sessionToken null || !sessionToken.equals(token)) { model.addAttribute(error, 请勿重复提交); return post/add; // 返回原页面 } // 2. 消费 token清空防止再次使用 session.removeAttribute(token); // 3. 保存业务数据 Post post new Post(); post.setContent(content); post.setUserId(((User) session.getAttribute(user)).getId()); postService.save(post); return redirect:/post/list; } }Step 3MySQL 建唯一索引兜底防极端并发-- 在 post 表中增加唯一约束假设用 contentuser_id 组合防刷 ALTER TABLE post ADD UNIQUE INDEX uk_content_user (content(200), user_id);逻辑说明JSP 生成 token 存 Session表单提交时携带Controller 比对后立即清除——这是最轻量的幂等方案MySQL 唯一索引是最后一道防线content(200)是前 200 字符截取避免长文本索引过大不用 Redis 是因为毕业设计环境通常无 Redis且单机 MySQL 足够应付并发。4. MySQL 数据库设计不是建完表就完事字符集、索引、外键的 3 个生死线4.1utf8mb4字符集 collation配置让“”和“张伟”都能存MySQL 默认utf8实际是utf8mb3最多 3 字节不支持 emoji需 4 字节。毕业设计若允许用户昵称带 emoji 或发帖含表情必须升级Step 1创建数据库时指定字符集CREATE DATABASE badminton DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Step 2修改表及字段字符集已有表需执行-- 修改表默认字符集 ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 修改特定字段如 nickname、content ALTER TABLE user MODIFY COLUMN nickname VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE post MODIFY COLUMN content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Step 3JDBC URL 强制指定已在 2.3 节给出jdbc:mysql://localhost:3306/badminton?useUnicodetruecharacterEncodingutf8mb4...逻辑说明utf8mb4_unicode_ci比utf8mb4_general_ci排序更准确尤其多语言混合时且兼容性更好CONVERT TO会重建表锁表时间长但毕业设计数据量小可接受单独MODIFY COLUMN比ALTER TABLE ... CHANGE COLUMN更安全不改变字段名。4.2 索引不是“越多越好”羽毛球平台的 4 个必建索引表名字段索引类型原因SQLuserphoneUNIQUE手机号注册唯一性强制校验ALTER TABLE user ADD UNIQUE INDEX uk_phone (phone);poststatus, create_time联合索引首页按“审核通过最新”排序避免 filesortALTER TABLE post ADD INDEX idx_status_time (status, create_time DESC);commentpost_id, create_time联合索引查某帖子所有评论并按时间倒序覆盖查询ALTER TABLE comment ADD INDEX idx_post_time (post_id, create_time DESC);court_bookingcourt_id, booking_date, start_time联合索引查某场地某天某时段是否可约精确匹配ALTER TABLE court_booking ADD INDEX idx_court_date_time (court_id, booking_date, start_time);逻辑说明user.phone建 UNIQUE 索引注册时SELECT COUNT(*) FROM user WHERE phone?就能秒级判断是否已存在不用查完再 Java 判断post.status, create_time联合索引当执行SELECT * FROM post WHERE status1 ORDER BY create_time DESC LIMIT 10时MySQL 直接用索引扫描不排序comment.post_id, create_time是典型“查询排序”场景联合索引让WHERE post_id? ORDER BY create_time DESC全索引覆盖court_booking的三字段索引确保WHERE court_id5 AND booking_date2024-06-01 AND start_time19:00能走索引避免全表扫描。4.3 外键不是“必须加”什么时候该用、什么时候该砍该用外键的场景保证数据一致性post.user_id→user.id帖子必须属于真实用户删用户时应级联删其帖子ON DELETE CASCADEcomment.post_id→post.id评论必须关联有效帖子删帖子时删评论ON DELETE CASCADE。不该用外键的场景性能/灵活性user.avatar_url头像地址是字符串可能存七牛云或本地路径无对应表强行建外键反而增加维护成本post.tags标签是逗号分隔字符串如双打,业余,南京非范式设计外键无意义。建外键 SQL 示例-- post 表外键 ALTER TABLE post ADD CONSTRAINT fk_post_user FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE; -- comment 表外键 ALTER TABLE comment ADD CONSTRAINT fk_comment_post FOREIGN KEY (post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE CASCADE;逻辑说明ON DELETE CASCADE是毕业设计常用策略避免“孤儿记录”如用户删了帖子还在ON UPDATE CASCADE保证主键更新时极少发生子表同步但毕业设计主键通常不更新可省略外键会降低 INSERT/UPDATE 性能需检查约束但数据量 10 万时感知不到且一致性价值远高于这点损耗。5. 避坑指南SSMJSPMySQL 羽毛球平台的 5 个血泪经验5.1 现象JSP 页面显示${user.nickname}为空但 Controller 里System.out.println(user.getNickname())有值原因JSP 中 EL 表达式默认从pageScope→requestScope→sessionScope→applicationScope顺序查找而 Controller 用model.addAttribute(user, user)是存到requestScope但页面跳转用了response.sendRedirect()重定向导致 request 作用域丢失。解决方案 A推荐Controller 返回String时用return user/profile;内部转发不要return redirect:/user/profile方案 B重定向时用RedirectAttributes传参attributes.addFlashAttribute(user, user)它会暂存到 Session下次 request 读取后自动清除。5.2 现象MySQL 插入中文显示???但SHOW VARIABLES LIKE character_set%全是utf8mb4原因JDBC URL 没加characterEncodingutf8mb4或 Tomcat 的server.xml中Connector没配URIEncodingUTF-8。解决检查 JDBC URL 必须含?useUnicodetruecharacterEncodingutf8mb4修改conf/server.xml在Connector port8080 ... /中添加URIEncodingUTF-8重启 Tomcat再测试。5.3 现象MyBatis 执行SELECT * FROM post WHERE status #{status}报错Parameter status not found原因Mapper 接口方法参数没加Param注解且方法只有一个参数此时 MyBatis 3.4 可省略但低版本或复杂参数必须加。解决// 错误写法无注解且 MyBatis 版本 3.4 ListPost selectByStatus(int status); // 正确写法加 Param ListPost selectByStatus(Param(status) int status);或升级 MyBatis 到 3.4 并确保mybatis-config.xml中setting nameuseColumnLabel valuetrue/。5.4 现象JSP 页面c:forEach items${posts} varpost报javax.el.PropertyNotFoundException: Property title not found原因Post实体类的 getter 方法命名不规范如getTitle()写成gettitle()或字段名title与 getter 不匹配如字段postTitle但 getter 是getTitle()。解决检查实体类字段private String title;必须对应public String getTitle() { return title; }使用 IDE 自动生成 getter/setterIntelliJ 快捷键AltInsert→Getter and Setter避免手写错误在mybatis-config.xml中开启日志setting namelogImpl valueSTDOUT_LOGGING/看 SQL 执行时是否报Could not find setter for title。5.5 现象Tomcat 启动报java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet原因Maven 依赖范围错误spring-webmvc依赖的scope被设为test或providedTomcat 已提供但实际需要打包进 WAR。解决检查pom.xml确保spring-webmvc无scope标签或为compiledependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.37/version !-- 删除 scopetest/scope 或 scopeprovided/scope -- /dependency清理 Mavenmvn clean compile package重新生成 WAR。6. 进阶技巧用 MySQL 视图 JSP 分页 SSM 缓存把羽毛球平台响应速度提 3 倍6.1 用 MySQL 视图简化复杂查询把“用户发帖数最新帖子标题”合成一张虚拟表羽毛球平台首页要显示每个用户的发帖总数和最新一条帖子标题传统做法是先SELECT * FROM user再对每个用户SELECT COUNT(*), MAX(title) FROM post WHERE user_id?—— N1 查询100 个用户就是 101 次数据库交互。用视图一步到位CREATE VIEW user_post_summary AS SELECT u.id AS user_id, u.username, u.avatar_url, COALESCE(p.post_count, 0) AS post_count, p.latest_title FROM user u LEFT JOIN ( SELECT user_id, COUNT(*) AS post_count, MAX(title) AS latest_title FROM post GROUP BY user_id ) p ON u.id p.user_id;Controller 直接查视图// UserPostSummary.java 实体类 public class UserPostSummary { private Long userId; private String username; private String avatarUrl; private Integer postCount; private String latestTitle; // getter/setter... } // DAO 接口 ListUserPostSummary selectAllUserSummary(); // Mapper XML select idselectAllUserSummary resultTypecom.yumao.entity.UserPostSummary SELECT * FROM user_post_summary ORDER BY post_count DESC LIMIT 20 /select优势一次查询返回全部数据无 N1视图逻辑在数据库层业务代码干净COALESCE(p.post_count, 0)处理 NULL避免 Java 空指针。6.2 JSP 原生分页不用插件5 行代码实现“上一页/下一页/跳转”!-- post/list.jsp -- c:choose c:when test${empty page.list} p暂无帖子/p /c:when c:otherwise c:forEach items${page.list} varpost div${post.title} - ${post.content}/div /c:forEach !-- 分页导航 -- div classpagination c:if test${page.pageNum 1} a href${pageContext.request.contextPath}/post/list?pageNum${page.pageNum-1}上一页/a /c:if span第 ${page.pageNum} 页共 ${page.pages} 页/span c:if test${page.pageNum page.pages} a href${pageContext.request.contextPath}/post/list?pageNum${page.pageNum1}下一页/a /c:if form action${pageContext.request.contextPath}/post/list methodget styledisplay:inline; input typenumber namepageNum min1 max${page.pages} value${page.pageNum} stylewidth:50px; button typesubmit跳转/button /form /div /c:otherwise /c:choose配套 Controller 分页逻辑RequestMapping(/post/list) public String listPosts(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); // PageHelper 开启分页 ListPost posts postService.selectAll(); // 此时 MyBatis 自动加 LIMIT PageInfoPost pageInfo new PageInfo(posts); // 封装分页信息 model.addAttribute(page, pageInfo); return post/list; }关键点PageHelper.startPage()必须在postService.selectAll()之前调用PageInfo对象自带pageNum、pages、list等属性JSP 直接${page.pageNum}取值跳转输入框min1 max${page.pages}防止用户输超范围页码。6.3 SSM 二级缓存实战MyBatis Ehcache让热门帖子查询快 10 倍MyBatis 一级缓存SqlSession 级默认开启但每次请求新建 SqlSession无效。二级缓存Mapper 级需手动开启Step 1添加 Ehcache 依赖dependency groupIdnet.sf.ehcache/groupId artifactIdehcache-core/artifactId version2.6.11/version /dependencyStep 2配置ehcache.xml!-- src/main/resources/ehcache.xml -- ehcache xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:noNamespaceSchemaLocationehcache.xsd diskStore pathjava.io.tmpdir/ defaultCache maxElementsInMemory1000 eternalfalse timeToIdleSeconds300 timeToLiveSeconds600 overflowToDisktrue diskPersistentfalse/ /ehcacheStep 3Mapper XML 开启缓存!-- mapper/PostMapper.xml -- mapper namespacecom.yumao.dao.PostDao !-- 开启二级缓存 -- cache typeorg.mybatis.caches.ehcache.EhcacheCache/ select idselectById resultTypecom.yumao.entity.Post useCachetrue SELECT * FROM post WHERE id #{id} /select /mapper效果验证首次访问/post/1SQL 打印日志第二次访问同一 ID日志无 SQL 输出说明命中缓存timeToLiveSeconds600表示缓存存活 10 分钟过期自动刷新。避坑提醒useCachetrue是默认值但显式写出更清晰实体类Post必须实现Serializable否则 Ehcache 序列化失败更新操作INSERT/UPDATE/DELETE会自动清空该 Mapper 的缓存无需手动干预。我带过 3 届毕业设计最常看到的翻车点不是技术不会而是“以为配好就完事”。比如web.xml里CharacterEncodingFilter放在DispatcherServlet后面结果中文参数永远乱码或者mybatis-config.xml忘了typeAliases导致resultTypePost找不到类。这些坑我都踩过现在养成习惯每改一个配置立刻用curl -X POST测接口看日志里 SQL 参数是不是对的。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站