
SSM景点门票预订管理系统算是JavaWeb阶段特别经典的一个完整案例了。前阵子为了带一个学弟走完整个课设流程我重新把JavaSSMJSPBootstrapMySQL这套组合从头到尾搭了一遍踩了不少坑也复盘了一些以前没仔细想清楚的细节。这篇文章就把整个项目的拆解思路、数据库设计、后端分层、前端页面整合以及我实际运行调试时遇到的一堆问题一次性记录下来。这套系统听起来就是个“增删改查”的毕业设计模板但真正动起手来你会发现它横跨了JavaWeb阶段的几乎所有核心知识点Maven依赖管理、SSM框架整合、JSPJSTLEL表达式、Bootstrap响应式布局、MySQL表设计与事务控制还有Tomcat部署和IDEA运行配置。把这套东西吃透后面学Spring Boot、MyBatis-Plus这些主流技术的时候会顺畅很多。先说结论吧这套项目适合三类人一是正在做课设或毕设的在校生需要一个功能完整、结构清晰、能跑通的参考案例二是想系统复习JavaWeb整合流程的开发者SSM虽然老但它的分层思想至今仍是主流框架的底子三是想学前后端协作的初学者JSPBootstrap的组合可以让你在没有Vue这类前端框架的情况下也能做出可用的管理界面。1. 项目梳理与技术选型1.1 这套系统到底要做什么景点门票预订系统核心目标是让用户能在线浏览景点信息、查看门票价格、在线下单预订同时让管理员可以管理景点、处理订单、查看统计。听起来简单但里面的业务逻辑一展开涉及的点还真不少。用户端的功能大概有注册登录、景点列表展示、景点详情查看、门票下单、个人订单查询、个人信息修改。管理端则包括景点分类管理、景点信息维护增删改查、订单审核与处理、用户列表管理。这些功能都是“标准件”任何一个做JavaWeb课设的人都能想象出来。但这个项目的价值不在于功能有多新鲜而在于它是怎么把这些功能用SSM这套框架组织起来的——Controller层怎么接收请求、Service层怎么处理业务、Mapper层怎么操作数据库、JSP页面怎么把这些数据渲染成用户能看的界面。这条链路梳理清楚你就掌握了JavaWeb项目的基本骨架。1.2 为什么选SSM而不是Spring Boot好多人在选题的时候会纠结现在都2025年了谁还学SSM呢直接上Spring Boot不香吗这个问题我也想过但从课程设计和学习路径来看SSM依然有不可替代的作用。SSM强调“手工装配”的过程。你要手动写Spring配置文件手动配置MyBatis的Mapper扫描手动在web.xml里注册SpringMVC的DispatcherServlet。这个过程很繁琐但正是这种繁琐逼着你搞懂了容器是怎么启动的、Bean是怎么注入的、请求是怎么被分发的。而Spring Boot把这些东西全自动了配置少了但你反而说不清楚背后发生了什么。如果你的目标是快速做课设Spring Boot确实效率更高。但如果你是想把JavaWeb底子打扎实SSM这套组合值得完整走一遍。我自己带学弟的时候就是让他先把SSM跑通再对比Spring Boot的自动配置很多概念一下子就通了。1.3 前端选型JSPBootstrap的搭配逻辑JSP在现在的前端生态里确实有点“复古”但它有一个很实际的优势服务端渲染数据直接在页面里通过EL表达式和JSTL标签输出不需要额外写接口文档、不需要处理跨域问题对初学者特别友好。Bootstrap的价值则在于“救急”。不用自己写复杂的CSS直接引入它的栅格系统、表单组件、模态框、分页组件就能拼出一个像模像样的后台管理界面。我做这个项目的时候就是先用Bootstrap搭了个简单的后台布局然后再加上自己的一些样式整体效果比裸写HTML好看太多。可以理解为JSP负责“动态”Bootstrap负责“好看”两者结合正是那个年代JavaWeb项目的标准配置放到今天的课设环境里依然完全够用。2. 开发环境准备IDEA、MySQL与Tomcat配置详解2.1 JDK与IDEA的版本搭配先说一个很多人容易忽略的点JDK版本和Tomcat版本是有兼容关系的。JDK 8对应Tomcat 8.5或9.x都没问题但如果你装的是JDK 17再用Tomcat 8.5就会报错。那我个人建议做SSM项目老老实实用JDK 8稳。IDEA方面我用的是IntelliJ IDEA社区版和旗舰版都行但旗舰版对JavaWeb的支持更完整。如果你用的是社区版需要自己装Smart Tomcat插件来运行项目旗舰版直接配置Tomcat Server就行。还有一个环境点是Maven。SSM项目依赖管理靠MavenIDEA里要么用内置的Maven要么自己下载一个配置好。我建议用自己下载的Maven然后修改settings.xml里的镜像源为阿里云镜像不然依赖下载慢到你怀疑人生。2.2 MySQL安装版本选择与初始化配置MySQL版本这地方确实容易踩坑。这个项目用MySQL 5.7或8.0都可以但注意两者的驱动类和连接URL写法不同5.7用的驱动是com.mysql.jdbc.Driver8.0要用com.mysql.cj.jdbc.Driver而且URL里要加上serverTimezoneAsia/Shanghai否则会报时区错误。如果你在Windows上安装MySQL 8.0我建议下载ZIP版而不是安装版。ZIP版解压后进入bin目录以管理员身份打开命令行按以下步骤初始化mysqld --initialize-insecure mysqld install net start mysql--initialize-insecure这个参数很关键它表示初始化时root用户密码为空方便首次登录。如果你直接用--initialize会生成一个随机密码日志里还很难找我第一次弄的时候翻了好半天才找到。安装好之后建议再装一个MySQL图形化管理工具我用的是Navicat当然也有人用免费的DBeaver或MySQL Workbench。Navicat用起来顺手但收费DBeaver免费开源功能也不差看个人习惯。注意MySQL初始化时如果命令行提示“服务无法启动”先别急着重装大概率是之前装过残留或者配置了错误的my.ini路径去服务管理器里看一下MySQL服务的可执行文件路径指向哪里再检查一下my.ini里的basedir和datadir是否写对了。从热词里看到大家常搜“RPM安装MySQL”“Docker安装MySQL失败”这个我也顺便说说。如果你用的是Linux服务器RPM包安装方式比较麻烦因为MySQL依赖很多我建议优先用官方Yum仓库安装。Docker安装MySQL其实更简单但很多人卡在容器启动后宿主机连不上那通常是因为没做端口映射或没用-p 3306:3306参数另外容器里的MySQL默认不允许空密码登录这一点跟本机安装完全不同。2.3 Tomcat配置与IDEA运行项目SSM项目最终是要丢进Tomcat里跑的IDEA配置Tomcat也有一套固定流程。我先在IDEA里配置好Tomcat Server然后在Deployment选项卡里添加war exploded最后设置Application context为项目名。这里有一个细节经常导致404项目访问路径没有配置对。如果你没有修改Application context那么你启动后访问的路径是http://localhost:8080/项目名_war_exploded/这个路径长得很难看。我一般把Application context改成/或/ticket这样URL就清爽多了。课程设计答辩的时候这个细节很加分评委一打开浏览器看到短路径第一印象就不一样。Debug模式启动Tomcat后记得控制台如果出现中文乱码八成是控制台编码问题。这个可以到Help → Edit Custom VM Options里添加-Dfile.encodingUTF-8同时在Tomcat的启动配置里把VM options也加上同样的参数基本就能解决。3. 数据库设计与核心表结构3.1 从业务需求到数据表的转化过程数据库设计是这个项目里最值得花时间琢磨的部分。我刚开始带人做项目的时候发现很多人上来就建表建到一半发现字段不对又回头改然后改SQL、改实体类、改Mapper来回折腾好几次。所以这里我建议先列业务功能清单再分析每个功能需要什么数据支撑最后再来建表。这个景点门票系统核心实体就四个用户User、景点分类Category、景点ScenicSpot、订单Order再加上一张评论表Comment作为扩展。用户和景点是多对多的关系但通过订单表关联所以订单表就作为中间表存在它还带了自己的属性下单时间、订单状态、支付金额等。原生的表结构我简化为这样CREATE TABLE t_user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_category ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_scenic ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT(11) NOT NULL, price DECIMAL(10,2) NOT NULL, description TEXT, image VARCHAR(255) DEFAULT NULL, opening_time VARCHAR(50) DEFAULT NULL, PRIMARY KEY (id), KEY idx_category_id (category_id), CONSTRAINT fk_scenic_category FOREIGN KEY (category_id) REFERENCES t_category (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_order ( id INT(11) NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT(11) NOT NULL, scenic_id INT(11) NOT NULL, ticket_count INT(11) NOT NULL DEFAULT 1, total_amount DECIMAL(10,2) NOT NULL, status TINYINT(1) NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user (id), CONSTRAINT fk_order_scenic FOREIGN KEY (scenic_id) REFERENCES t_scenic (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;3.2 字段设计与索引设计的几点思考上面这套表设计看起来简单但每个地方都有讲究。我逐一说说。用户表的密码字段用了VARCHAR(100)不是VARCHAR(20)是因为如果以后想用MD5加盐或BCrypt加密密文长度会远大于明文。我实际做这个项目的时候就吃过亏一开始把password设为VARCHAR(20)后来改成MD5加密存进去直接怎么都等值匹配不上回头一查才醒过来开始截断了。订单表的order_no字段我设置了唯一索引因为订单号在业务上必须唯一用它做查询也合适。注意订单号让我用时间戳加随机数生成而不是直接用数据库自增ID因为自增ID很容易被猜到你下了一单就能推测出系统大概有多少订单。status字段用的是TINYINT0表示待支付1表示已支付2表示已取消。用数字存状态的好处是简洁、可扩展但代价是代码里要写一堆判断逻辑。我为这个项目专门写了一个订单状态常量类不然代码里到处是魔法数字维护起来很痛苦。景点表的price字段用了DECIMAL(10,2)这是对金钱字段的基本尊重。用FLOAT或DOUBLE存金额等到计算总价的时候会出现意想不到的精度问题因为浮点数在计算机里本来就是近似值。3.3 Navicat导入与连接配置表建好之后用Navicat执行SQL文件导入即可。这里有一个我反复遇到的问题就是导入时提示[ERR] [MY-014060] [SERVER] INVALID MYSQL SERVER UPGRADE这个错误一般出现在MySQL 8.0.x导出的SQL文件在另一个版本的MySQL上执行时。解决方案有两个一是尽量在同一个版本系列的MySQL环境里导入导出二是如果出现了这个错误就先单独执行创建库和创建表的语句跳过版本检查那一句通常在文件开头的注释部分。一旦出现“invalid mysql server upgrade”这种提示不代表整个文件没法用了很可能是开关即某一行的版本标记SQL不被兼容删掉那几行再执行就行。还有个连接层面的坑Java连接MySQL 8.0的时候如果URL参数没有指定useSSLfalse控制台会报SSL连接错误。这个不用紧张直接加上参数就好jdbc:mysql://localhost:3306/ticket_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai不能少否则默认时区UTC和本地时间差8个小时你插入一条数据会发现自己存的时间整整晚了8小时。4. SSM整合与核心代码实现4.1 项目结构与依赖配置说句实在话SSM项目最劝退新手的就是那堆配置文件。你可能花两天就会写Java代码了但配置Spring、SpringMVC、MyBatis三者之间的关系就能让你再卡一个星期。所以这个章节我把核心配置拆开了揉碎了讲。先说项目的标准目录结构。按maven的标准布局src/main/java下面按包名组织类src/main/resources下放配置文件src/main/webapp下放JSP页面和静态资源。Java层面的分包我习惯这样做controller接收请求、返回视图service业务逻辑接口service.impl业务逻辑实现dao数据访问接口也就是Mapperentity实体类common公共工具类、常量类不要小看分包规范。做过实际项目的人都知道包结构混乱的代码过两周自己都找不到自己写的类在哪。规范分包对于课设答辩时演示代码也有很大帮助评委翻你的源码就知道你懂不懂工程化组织。pom.xml依赖方面这个项目至少要引入这些dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.10.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.2.10.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.23/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies注意MySQL驱动版本要和数据库版本匹配。5.7的库用5.1.x驱动8.0的库用8.0.x驱动。驱动版本选不对最直观的现象就是报ClassNotFoundException或Communications link failure。4.2 核心配置文件逐行解读SSM三大框架需要一个“总集成”的思路。我个人的理解是Spring负责管一切BeanSpringMVC负责管Controller层MyBatis负责管数据库操作。它们不是三个独立的东西而是用SpringMyBatis的桥接包mybatis-spring把三者串起来。applicationContext.xml是Spring的根配置文件核心内容如下context:component-scan base-packagecom.example.ticket context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan context:property-placeholder locationclasspath:db.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.example.ticket.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.ticket.dao/ /bean tx:annotation-driven transaction-managertransactionManager/这里有几个容易出问题的地方。第一context:component-scan配置了一次扫描但排除了Controller因为Controller要交给SpringMVC的配置去管理如果两边都扫描到同一个类会产生重复Bean的警告。第二Druid连接池的driverClassName不需要加driver了8.0版本驱动它会自动识别。第三typeAliasesPackage配好后MyBatis的Mapper XML里写resultType时就可以直接写类名不用写全限定名短很多。springmvc.xml配置如下context:component-scan base-packagecom.example.ticket.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:resources mapping/static/** location/static//视图解析器的配置决定了Controller return的字符串对应到哪个JSP页面。比如Controller里return scenic/list最终渲染的页面就是/WEB-INF/jsp/scenic/list.jsp。web.xml是JavaWeb项目的入口配置作用是启动时加载Spring容器、注册SpringMVC前端控制器context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping/这个路径映射很有意思它表示所有请求都交给DispatcherServlet处理除了JSP文件本身Tomcat的JspServlet会单独处理。很多人设置url-pattern为*.do这样也行但URL就不如这种简洁号看了。我用/然后在Controller里用RequestMapping精确匹配这就是REST风格的前身。4.3 Service层的事务管理与订单业务订单创建是这个系统里最核心的业务它涉及多步数据库操作检查库存、扣减库存这里景点没有库存但可以约定每日售票数、插入订单记录、更新用户积分如果有的话。只要涉及多步写操作就必须考虑事务。Spring的事务管理在SSM里用Transactional注解就能搞定我在OrderService的createOrder方法上加了注解Override Transactional(rollbackFor Exception.class) public boolean createOrder(Order order) { // 校验景点是否存在 ScenicSpot spot scenicSpotDao.selectById(order.getScenicId()); if (spot null) { throw new ServiceException(景点不存在); } // 计算总金额 order.setTotalAmount(spot.getPrice().multiply(new BigDecimal(order.getTicketCount()))); // 生成订单号时间戳随机数 order.setOrderNo(generateOrderNo()); // 插入订单 int rows orderDao.insert(order); return rows 0; }rollbackFor Exception.class特别重要。如果你不指定这个参数Spring默认只在RuntimeException时回滚而受检异常比如ServiceException默认是不回滚的。如果订单插入成功、但后续更新库存失败那就会出现数据不一致。这个细节面试和答辩都经常问理解到位能加分不少。generateOrderNo()我用了SimpleDateFormat格式化当前时间再加一个4位随机数最终组成类似202505211430221234的订单号。严格来说在高并发场景下可能重复但课设场景完全够用没必要上Redis自增。4.4 MyBatis映射与动态SQLMyBatis的核心优势是SQL是你自己控制的这一点比JPA的自动化更有掌控感。门票系统的Mapper文件里比较有代表性的是景点查询和订单查询前者的查询条件是动态的select idsearchScenic resultTypeScenicSpot SELECT * FROM t_scenic where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC /selectwhere标签很聪明如果所有条件都不成立它不会生成WHERE关键字如果第一个条件成立、第二个也成立它会自动处理多余的AND。这种写法比拼字符串安全得多也能防SQL注入因为#{}是预编译占位符。订单列表的分页查询也是MyBatis里很常见的写法。我一开始用的是LIMIT #{offset}, #{pageSize}后来引入了PageHelper插件一行代码搞定分页PageHelper.startPage(pageNum, pageSize); ListOrder orders orderDao.selectUserOrders(userId); PageInfoOrder pageInfo new PageInfo(orders);PageHelper的原理是在执行SQL之前自动拼接LIMIT语句用起来非常方便。市面上的课设项目绝大多数都用了它如果你不想引入插件也可以自己写分页查询原理是一样的。5. JSP页面与Bootstrap前端整合5.1 用户端的核心页面实现我从大家搜的热词里摘一个来说JSP个人信息展示页面。这个页面看着简单但它能体现你是否理解了会话管理、数据回显、表单校验这几个JavaWeb的核心概念。用户登录成功后一般把用户对象存在session里PostMapping(/login) public String login(String username, String password, HttpSession session) { User user userService.login(username, password); session.setAttribute(loginUser, user); return redirect:/scenic/list; }然后JSP页面里取数据div classcard div classcard-body h5 classcard-titlec:out value${sessionScope.loginUser.username}//h5 p classcard-text邮箱${sessionScope.loginUser.email}/p p classcard-text电话${sessionScope.loginUser.phone}/p /div /div这里要提醒一个XSS安全陷阱。如果用户注册时在邮箱字段里输入了HTML代码直接用${user.email}输出页面会把那段代码当HTML渲染。正确做法是用c:out标签它默认做HTML转义输出lt;scriptgt;这样的转义字面量而不是可执行的脚本。这个细节在真实开发中很重要答辩时说出来也很提专业分。个人信息修改页面我是用表单提交的方式做的没有上Ajax理由很简单SSM的Controller直接返回视图路径表单提交最直观也最容易理解。如果你想练手感也可以改用Ajax提交JSON然后返回一个提示信息这个看个人诉求。5.2 后台管理界面的布局设计管理端的JSP页面我用的是一个经典的Bootstrap后台模板布局左侧导航栏、右侧内容区。这个布局用Bootstrap的栅格系统加Flexbox就能拼出来不用引入复杂模板。导航栏里放的功能入口要跟管理员的Controller对应起来景点分类管理/admin/category/list景点信息管理/admin/scenic/list订单管理/admin/order/list用户管理/admin/user/list页面顶部固定一个导航条用Bootstrap的navbar组件右侧放管理员退出按钮。每个管理页面的主体部分就是一个table表格加一个分页栏。表格用Bootstrap的标准类table table-striped table-hover行间色块和悬停效果都是自带不用自己写CSS。我后来为了让界面更精致还引入了Element图标库的前端版本。有朋友说“饿了么Element图标前端JSP怎么用”其实简单得很你不需要引入Vue只要把Element的图标字体CSS文件引进来然后在JSP标签里直接写等了写等i classel-icon-edit/i 编辑 i classel-icon-delete/i 删除 i classel-icon-plus/i 新增图标马上就有统一的视觉风格比自己写图片或字符好看得多。在Bootstrap基础上叠加Element的图标是很多JavaWeb管理系统的常见操作成本低效果好。5.3 景点列表分页与搜索景区列表是用户端的核心页面。我用PageHelper做了分页前端通过c:forEach渲染每页的数据同时用JSTL的c:if控制分页按钮的显示状态。这里有一个体验细节分页搜索时点击第二页会丢失搜索条件。原因是分页链接里没拼上搜索关键词。解决方式是在生成分页URL的时候把查询条件一块带上a href${pageContext.request.contextPath}/scenic/list?pageNum${pageInfo.nextPage}keyword${param.keyword}下一页/a${param.keyword}是EL表达式里获取请求参数的方式比request.getParameter(keyword)简洁太多。这个细节不处理好的话用户搜了个关键词、翻到第二页突然看到全部景点会以为系统出了Bug。5.4 表单验证与页面刷新问题关于JSP页面“加载完后刷新一次”这个热搜词我大概明白在找什么因为以前也遇到过。有些页面在表单提交后用了forward方式跳转Controller直接return视图路径地址栏没变化这时用户按F5刷新浏览器会重复提交同一个POST请求导致重复下单或重复评论。类似“JSP页面加载完后刷新一次”本质上就是在处理forward跳转带来的重复提交问题。它的解决方案其实很标准用redirect方式代替forward。Controller里return redirect:/scenic/list浏览器会先发一个302响应再重新GET目标地址这样地址栏就变成了GET的URL刷新也不会重复提交。这个叫Post/Redirect/Get模式PRG模式做JavaWeb开发一定要记住。我见过很多人的课设代码最后一个订单能买好几张同款票就是没做这个处理。6. 常见问题与排查实录6.1 IDEA运行JavaWeb项目的经典报错这部分是血泪经验总结。很多人下载了一个完整的JavaWeb项目导入IDEA后各种报错其实大多数都不是代码问题而是环境配置问题。第一个经典问题是Tomcat启动后访问404。排查顺序先看控制台日志有没有报异常如果没有异常那就是访问路径不对检查Application context配置如果有异常那一般是类没有部署到Tomcat的WEB-INF/classes目录里IDEA的Artifacts配置有问题重新选一次war exploded通常能解决。第二个常见问题是java.lang.NoClassDefFoundError。这个错误的意思是类在编译时存在但运行时找不到一般是因为依赖包的scope配置错了。比如javax.servlet-api的scope必须是provided因为Tomcat自带Servlet JAR如果设置成了compile并把JAR打进了WEB-INF/lib就会和Tomcat自身的类冲突。第三个问题集中在中文乱码上。请求乱码、响应乱码、控制台乱码三种乱码原因各不相同。请求乱码要设置CharacterEncodingFilter过滤器强制把所有请求编码设为UTF-8filter 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 /filter不过这个过滤器只能处理POST请求的编码GET请求的乱码还得去改Tomcat的server.xml给Connector节点加上URIEncodingUTF-8双管齐下才能彻底解决。6.2 数据库连接与MySQL异常处理数据库相关的坑我用一段时间列出来都是实际项目里高频遇到的。报Access denied for user rootlocalhost说明密码不对。刚装完MySQL如果用了--initialize-insecureroot密码为空连接时直接留空就行。如果你尝试了很多密码都不对可以改用--skip-grant-tables方式启动MySQL免密登录后重置root密码再把那行参数去掉重启。报Communications link failure或Connection timed out先检查MySQL服务是否在运行。在Windows环境用命令行net start | findstr mysql查看服务状态如果是远程连接数据库还要检查防火墙是否放行了3306端口云服务器还要注意安全组设置。报Public Key Retrieval is not allowed这是MySQL 8.0的坑。用户用了caching_sha2_password插件做认证而这个认证方式需要先获取公钥。最简单的方法是连接URL上加allowPublicKeyRetrievaltrue或者把JDBC驱动升级到8.0.13以上同时加上这个参数。另一种思路是把用户认证插件改回老的mysql_native_password但新版本的MySQL已经在逐步废弃这个插件不如直接适应新的做法。还有一个热词是“Net start MySQL服务无法启动”。这种服务启动失败的情况先看Windows事件查看器里的MySQL日志常见原因有datadir指定的目录不存在或无权限、my.ini配置项写错端口导致冲突、之前的非正常卸载残留了注册表项。我的处理顺序是先删掉MySQL服务sc delete mysql再重新解压一份干净的MySQL目录初始化后重新安装服务。6.3 数据库连接池与资源释放这个项目里我用了Druid连接池阿里巴巴开源的那款。选择它的原因一个是性能好另一个是监控页面好用你可以在浏览器里直接打开/druid/index.html查看当前SQL执行情况和连接池状态。配置Druid时有一个我常提醒的点要保证关闭数据源时能释放连接。虽然SSM框架会帮我们管理但如果你自己在代码里写了获取连接的逻辑记得在finally块里关闭Connection、PreparedStatement、ResultSet三个资源。Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn dataSource.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); } finally { if (rs ! null) try { rs.close(); } catch (SQLException e) { } if (ps ! null) try { ps.close(); } catch (SQLException e) { } if (conn ! null) try { conn.close(); } catch (SQLException e) { } }连接泄漏是JavaWeb项目最常见的隐性问题之一表现就是跑一段时间后页面访问越来越慢最后直接报连接超时。如果你的项目也出现这种情况先检查有没有哪里写了getConnection()却没close()。用Druid的监控页面可以直观看到当前活跃连接数和空闲连接数排查起来效率高不少。6.4 远程部署与版本兼容问题做课设的时候作品可能不止在本机运行还要部署到别人的电脑或云服务器上演示。这里容易踩两个坑。第一个是JDK版本。本机用的是JDK 8服务器上装的可能是JDK 11或17直接跑会报UnsupportedClassVersionError。去服务器上用java -version查一下确保主版本号一致就行。第二个是MySQL版本跨版本导入问题。本机是MySQL 8.0服务器是MySQL 5.7导出的SQL文件里可能包含8.0特有的语法比如utf8mb4_0900_ai_ci排序规则5.7执行时就报错。我的习惯是导出SQL时选用“兼容”模式或者干脆手写建表语句确保两边都能执行。7. MyBatis常用注解与Mapper映射方式对比7.1 XML与注解两种方式的取舍SSM项目里MyBatis写SQL有两种方式XML文件和注解。在简单场景下两者都能用但我的建议是核心业务用XML简单查询可以适当用注解。注解方式适合单表简单查询比如public interface CategoryDao { Select(SELECT * FROM t_category WHERE id #{id}) Category selectById(Integer id); }只要SQL不复杂注解方式写起来确实快。但一旦涉及动态SQLif判断条件、foreach遍历集合注解方式就会变得很难看你得在一大串字符串里拼条件缩进和引号稍不留神就出错错误处理起来特别费劲。这时候XML方式的优势就体现出来了语法更好看能写注释还能复用SQL片段。7.2 高频注解速查结合大家搜的“SSM常用注解”我把这个项目里实际用到的注解做一个梳理。Spring部分Component通用组件注解DAO、Service、Controller之外的类用Service标注Service实现类被Spring容器管理Autowired按类型自动注入依赖Resource按名称注入实际工作中偶尔会混用Transactional事务管理注解用在Service层方法上Value读取配置文件的值SpringMVC部分Controller标注Controller类RequestMapping映射请求URL可以加HTTP方法限制RequestParam获取请求参数并绑定到方法参数PathVariable从URL路径模板中提取参数ResponseBody方法返回对象直接序列化成JSON响应体ModelAttribute暴露表单对象到Model中MyBatis部分Mapper标注Dao接口让MyBatis生成代理实现Select、Insert、Update、Delete四种操作SQL简写Param标注Mapper方法参数名用于SQL中的参数占位这些注解背下来不难关键是要弄清楚它们各自在什么场景下生效。比如ResponseBody你用在了Controller的方法上返回的字符串就会被当作文本响应而不是视图名没加这个注解返回的字符串就会被当作JSP路径去解析最后页面报404。很多初学者在这上面卡很久其实就是没理解SpringMVC默认把返回值当视图名。7.3 Mapper接口与SQL映射文件如何对应还有一点值得展开说的是Mapper接口的方法名和XML映射文件的id要怎么对应。MyBatis用接口的全限定名加方法名来定位SQLpublic interface ScenicSpotDao { ListScenicSpot selectList(PageParam param); }对应的XML文件里select idselectList resultTypeScenicSpot SELECT * FROM t_scenic /select如果接口方法名和XML里的id对不上启动时就会报Invalid bound statement (not found)错误这是SSM项目里最让人抓狂的错误之一。出现这种错误时按以下顺序排查先看XML文件的namespace是否等于接口的全限定名包括包名再看id是否等于方法名再看mapperLocations是否扫描到了该XML文件配置文件路径是否正确。这个错误我有一次弄了快两个小时最后发现是mapper目录下的XML文件名跟别人的重复了MyBatis按顺序扫描时加载了错误的那份。从那以后我建Mapper文件的时候都会先看一眼目录里是否已存在同名文件。8. 从SSM到Spring Boot的技术迁移视角8.1 先说清楚SSM的价值在哪里每次写这类项目复盘帖的时候都会有人问同一个问题SSM过时了值不值得花这么大力气学现在Spring Boot MyBatis-Plus才是主流SSM还有必要吗我从实际带新人的感受出发说点实话。SSM的真正价值不是让你在工作中去用它而是让你理解框架组合时的“手动挡”过程。Spring Boot把所有东西自动配置好了你会很舒服但也容易产生依赖——项目出了问题你只知道百度搜“Spring Boot xxx报错”不懂它是怎么装配起来的。而你自己动手配过一遍SSM至少知道DispatcherServlet在哪、SqlSessionFactory是干什么的、Transactional背后的代理机制是什么。8.2 把SSM项目平移成Spring Boot的思路如果你做完SSM这个项目想再进一步可以尝试把同一个业务逻辑用Spring Boot重写一遍。其实改造成本不高大概两周左右能完成。Spring Boot里applicationContext.xml和springmvc.xml几乎可以删掉换成application.yml里的配置spring: datasource: url: jdbc:mysql://localhost:3306/ticket_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mvc: view: prefix: /WEB-INF/jsp/ suffix: .jspController代码基本不用改动Service和Dao也是。你只需要加一个SpringBootApplication启动类在pom里换掉Spring的依赖让Maven帮你去管理版本。这就是我常说的“SSM项目是Spring Boot的前置练习”逻辑上的迁移成本非常低。前端如果还想升级一档可以把JSP换成Vue Element UI的前后端分离模式后端只提供JSON接口但那就是另一个话题了。8.3 项目演示时的加分项答辩或演示时有几个细节可以体现出你的专业度供参考。一是在系统里预留一个演示账号降低入场操作成本。管理员账号密码写在一个名为README的文档里和别人共享项目的时候也不至于被反复问。二是做一份简短的演示脚本按顺序演示用户注册→用户登录→浏览景点→下单→查看订单→管理员登录→审核订单→新增景点这样一条线走下来逻辑顺畅也不容易漏功能。三是准备一页数据库设计文档写出每张表的作用以及表之间的外键关系。用文字或图说明都行但不用画太复杂的E-R图只要能表达清楚用户与订单的关联、景点与分类的归属关系就很够用了。9. 写在最后的运行心得项目跑通之后我复盘了整个流程最大的体会是SSM这套技术栈虽然不算新但它依然是JavaWeb学习者绕不开的一项基本功。骨架是Servlet规范肌肉是Spring生态骨骼是MyBatis的SQL掌控感。把这个项目做透你对Java后端开发的理解会比直接上手Spring Boot的人扎实不少。最后再分享一个小技巧如果是自己练习或做课设可以在项目里加一个全局异常处理器把所有未捕获的异常统一跳转到友好的错误页面而不是把异常栈直接甩给用户。这个细节虽然不能提升系统功能但能立刻让你的项目看起来比同级同学的作品专业一个档次。具体做法是在SpringMVC配置类里写一个ControllerAdvice类结合ExceptionHandler处理业务异常再配一个ResponseBody返回JSON或直接返回错误视图。我个人的体会是做这种课程设计项目别指望一步到位。先把核心功能跑通再逐渐补充异常处理、表单校验、分页优化这些细节。做这类项目的最终收获永远不只是那几张小成绩表而是你真正理解了“一个请求从浏览器出发到底是怎么一步步变成数据库里的一个变更记录”的完整链条。这个链条想明白了后面的技术栈再怎么变你也知道它往哪个方向改。