简介基于Spring MVCSpring与MyBatis整合的学生管理管理系统是一套经典Java Web课程设计项目适合初学者、高校学生以及需要SSM框架实战的开发者。系统覆盖学生信息的模糊查询、新增、删除等核心操作前端采用JSP与Servlet协作后端通过Spring管理业务对象MyBatis负责数据库访问整体结构清晰便于理解分层开发思想。资源包共包含95个文件压缩包大小约36.14MB其中包含58个JAR依赖包、12个XML配置文件、5个Java源文件、3个JSP页面以及MySQL数据库脚本、PDF需求说明和Eclipse项目配置文件等目录规范可直接导入MyEclipse或Eclipse配合Tomcat运行。目前已有2339人学习下载适合需要完整练手项目作为参照的开发者。借助这份资源可以获得一套可运行的学生管理系统源码配合MySQL数据库脚本快速建表结合PDF题目说明掌握功能需求同时在阅读源码过程中能积累SSM整合、MyBatis映射、JSP页面渲染和连接池配置等关键经验也方便在此基础上进行二次开发完善其他业务模块。1. 一个SSM学生管理系统值得手动拆一遍刚解压这个项目时第一感受是这不是Spring Boot那种一步到位的自动配置而是把Spring MVC、Spring、MyBatis三套XML全部摊开的老式Eclipse工程。但也正因如此它能让人看清楚DispatcherServlet如何接管请求、SqlSessionFactory如何被Spring托管、事务边界又划在哪个层。项目功能不复杂围绕学生信息的模糊查询、新增、删除展开配合MySQL和Tomcat就能完整跑通。对准备面试的人这是一个能现场讲清IoC、AOP和MyBatis动态SQL的活素材对想把历史SSM项目迁到Spring Boot的工程师也值得先把这套XML配置读透再动手。2. 配置骨架web.xml、数据源与MyBatis FactoryBean2.1 目录结构与容器的双层初始化解压后的包体是一个标准动态Web工程源码在src下页面在WebContent下WEB-INF里是web.xml和编译后的class。典型分层是com.controller、com.service、com.mapper、com.entity。注意build/classes目录不是源码是Eclipse编译输出部署时不要手动复制这个目录否则容易带着旧class和当前源码不一致。Tomcat启动时web.xml是入口。这里包含了两个Spring容器一个由ContextLoaderListener创建的根容器负责Service、Mapper、DataSource等业务组件一个由DispatcherServlet创建的MVC容器只负责Controller和视图解析。之所以分两层是为了让MVC容器不重复创建Service也可以让多个Servlet共享根容器中的公共bean。context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup设为1表示应用加载时立即初始化DispatcherServlet而不是等第一个请求来再初始化能更快地暴露配置错误。/的映射覆盖了默认静态资源处理器所以spring-mvc.xml里必须显式放行否则JSP里引用的CSS会全部404。2.2 数据源与MyBatis会话工厂的整合这个项目的数据库连接参数集中在jdbc.propertiesSpring用context:property-placeholder读取。数据源选用C3P0MySQL驱动版本需要与MySQL版本匹配常见报错是驱动类com.mysql.jdbc.Driver在新版MySQL 8下不存在要改成com.mysql.cj.jdbc.Driver并增加时区参数。数据源、SqlSessionFactoryBean、MapperScannerConfigurer三个bean按顺序配置context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource property namedriverClass value${jdbc.driver}/ property namejdbcUrl value${jdbc.url}/ property nameuser value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:com/mapper/*.xml/ property nametypeAliasesPackage valuecom.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.mapper/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /beanmapperLocations指向所有Mapper XML路径写错时项目启动不报错首次调用对应Mapper方法才提示Invalid bound statement。typeAliasesPackage让resultTypeStudent可以替代全限定名但同名类在不同包时会冲突业务复杂后建议写全限定名。MapperScannerConfigurer的sqlSessionFactoryBeanName用了字符串而不是ref目的是避免提前初始化工厂导致占位符未解析。以前我遇到过DataSource密码是${jdbc.password}原样传入数据库的情况就是因为PropertyPlaceholder加载顺序不对。2.3 Spring MVC视图解析与常见参数对照spring-mvc.xml中开启mvc:annotation-driven/然后配置视图解析器。项目使用JSP作为视图层Controller返回字符串解析器自动拼接前后缀context:component-scan base-packagecom.controller/ mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean把JSP放WEB-INF/views下用户直接输入页面路径也无法访问所有页面渲染必须经过Controller。对于这个项目来说静态资源不多default-servlet-handler足够有更多静态资源时应该配mvc:resources并设置缓存期。这里有一个比较容易让新手困惑的点annotation-driven和default-servlet-handler同时配置时注解驱动的HandlerMapping仍然优先处理RequestMapping所以不会冲突。配置层还有一类容易踩的坑是事务。如果Spring事务管理器没有纳入IoC容器Transactional不会生效。常见配置是针对数据源声明DataSourceTransactionManager然后在根容器里开启tx:annotation-driven/。事务管理器要放在根容器而不是MVC容器否则Service代理可能扫描不到。配置项实际作用没配/配错的现场contextConfigLocation指定根容器配置文件位置启动后Service为nullmapperLocations告诉MyBatis去哪找SQL映射方法调用时“Invalid bound statement”typeAliasesPackage为实体类提供短别名resultType编译期不报错运行期可能类型解析失败sqlSessionFactoryBeanName延迟注入Factory实例数据库密码未解析连库报Access deniedtx:annotation-driven开启动态代理事务删除成绩和删除学生只执行一半无回滚手动整合SSM时每个容器的边界都要想清楚。Spring的IoC容器在启动时创建单例bean无状态的Service天然适合单例这也是为什么后来Spring Boot大量沿用这种模式。老项目里如果Service之间互相注入形成循环依赖Spring会通过三级缓存提前暴露半成品对象来解决但SSM的XML配置阶段处理循环依赖比注解时代更隐晦最好的做法还是拆分接口避免两个实现类互相持有。3. 从Controller到Mapper新增和删除的执行链路3.1 Controller参数绑定请求参数到Java对象学生管理系统的Controller方法一般都很短核心工作是把页面参数取出来传给Service。Spring MVC用HandlerAdapter完成参数绑定方法入参支持简单类型、POJO、Model、RedirectAttributes等。Controller RequestMapping(/student) public class StudentController { private final StudentService studentService; Autowired public StudentController(StudentService studentService) { this.studentService studentService; } RequestMapping(/list) public String list(String name, Integer clazzId, Model model) { model.addAttribute(list, studentService.findByCondition(name, clazzId)); return student/list; } RequestMapping(value /add, method RequestMethod.POST) public String add(Student student, RedirectAttributes attr) { studentService.saveStudent(student); attr.addFlashAttribute(msg, 新增成功); return redirect:/student/list; } RequestMapping(value /delete, method RequestMethod.POST) public String delete(Integer id) { studentService.deleteStudent(id); return redirect:/student/list; } }构造器注入相比字段Autowired更方便单元测试也能在编译期发现依赖缺失。add方法使用POST提交表单里name、birthday等字段名与Student属性一致Spring MVC会反射调用setter完成绑定。如果前端传了多余字段默认情况下会忽略不会报错。delete用POST而不是GET避免了链接爬虫或预读插件误删数据这是一个很多老项目没有做好的细节。重定向用RedirectAttributes.addFlashAttribute携带提示信息刷新列表页后提示不会残留。3.2 Service事务与Mapper接口的约定Service实现类上的Transactional决定了事务边界。新增学生时直接插入student表删除学生时如果先删成绩表再删学生中间任何一个失败都要整体回滚所以删除逻辑放在一个事务方法里Service Transactional public class StudentServiceImpl implements StudentService { private final StudentMapper studentMapper; public StudentServiceImpl(StudentMapper studentMapper) { this.studentMapper studentMapper; } public void deleteStudent(Integer id) { studentMapper.deleteScores(id); studentMapper.deleteStudent(id); } }Transactional默认只回滚RuntimeException和Error。如果SQL抛的是受检异常事务不会自动回滚。MyBatis Mapper接口在这里发挥了一个动态代理的作用没有实现类的接口如何在Spring容器中存在这是MyBatis与Spring整合的核心MapperFactoryBean为每个接口生成代理方法调用时从SqlSessionTemplate获取会话绑定SQL并执行。Mapper XML的namespace必须和接口全限定名对应mapper namespacecom.mapper.StudentMapper insert idinsertStudent parameterTypeStudent insert into student(name, gender, birthday, clazz_id) values(#{name}, #{gender}, #{birthday}, #{clazzId}) /insert delete iddeleteStudent delete from student where id #{id} /delete delete iddeleteScores delete from score where student_id #{id} /delete /mapperparameterTypeStudent可以省略MyBatis会根据Mapper接口泛型推断。#{clazzId}对应实体的getClazzId()方法。注意数据库列名clazz_id与Java属性clazzId通过mapUnderscoreToCamelCase自动转换如果没有开启需要为查询列设置别名。MySQL外键约束开启时删除父表记录前必须先处理子表顺序反了会报外键约束错误。3.3 列表页的JSP渲染与上下文路径Controller返回student/list后InternalResourceViewResolver定位到/WEB-INF/views/student/list.jsp。JSP里用JSTL的c:forEach循环基础代码% taglib urihttp://java.sun.com/jsp/jstl/core prefixc % table trth学号/thth姓名/thth班级/thth操作/th/tr c:forEach items${list} vars tr td${s.studentId}/td td${s.name}/td td${s.clazzId}/td td form action${pageContext.request.contextPath}/student/delete methodpost styledisplay:inline input typehidden nameid value${s.studentId}/ button typesubmit删除/button /form /td /tr /c:forEach /table删除使用内联form而不是a链接就是为了服务端要求POST。${pageContext.request.contextPath}动态获取应用上下文项目部署名改成studentms时链接依然正确。如果JSP页面上某个EL表达式解析为空首先检查User实体是否提供了对应getter其次检查Controller放进Model的key是否与页面一致。这里还有一个常见体验问题列表页表格数据很多时不要用c:forEach直接遍历几千行应该在SQL层做分页减少网络传输和DOM渲染压力。4. 模糊查询的SQL拼法与MyBatis动态SQL边界4.1 为什么不推荐LIKE %${name}%学生管理系统的查询条件一般为姓名、班级。很多人写的第一个版本是like %${name}%运行时确实能出结果但把用户输入直接拼进SQL属于典型的注入入口。假设用户输入a or 11最终SQL会变成select * from student where name like %a or 11%这会绕过原查询条件导致全表返回。MyBatis中#{}对应JDBC的?占位符MySQL驱动会把参数通过PreparedStatement传入#{}在LIKE里的安全写法是用函数拼接select idfindByCondition resultTypecom.entity.Student select * from student where if testname ! null and name ! and name like concat(%, #{name}, %) /if if testclazzId ! null and clazz_id #{clazzId} /if /where order by student_id desc /selectconcat(%, #{name}, %)生成的预编译SQL是name like concat(%, ?, %)参数值不会被当作SQL指令执行。where标签自动处理第一个条件前面的and不需要where 11这种反优化写法。4.2 动态SQL中空值判断与字符串判断的坑if判断空值的表达式不要把字符串和非空判断混在一起。我在实际项目中见过这样的错误写法if testname ! 如果name为nullOGNL会尝试比较null和空字符串部分版本会返回true导致SQL多出一个永远失败的条件。标准写法是name ! null and name ! 。如果前端没有提交name字段Controller入参为null直接进入if判断即可如果前端提交空字符串需要空串判断也过滤掉。对于班级ID这类Integer参数只需要clazzId ! null。业务上如果允许查询“未分班”的学生可能用clazzId -1表达但注意if testclazzId ! null仍然会参与条件与不在条件中不同。4.3#{}和${}的适用边界MyBatis中${}是字符串替换#{}是预编译参数。在普通查询值上应该一律用#{}。但有些SQL结构无法用占位符替代比如动态排序列名if testorderBy ! null order by ${orderBy} ${direction} /if这种位置用#{}会生成order by ?MySQL不接受参数作为列名。这里的${orderBy}必须通过白名单校验String safeOrder student_id; if (name.equals(param.get(orderBy))) { safeOrder name; }排序方向同样只接受asc或desc。白名单之外的任何值都回退到默认排序。表格对比一下两种占位符的差异占位符处理方式适用场景风险#{}PreparedStatement占位符自动加引号值比较、LIKE内容、IN集合项低${}纯字符串拼接表名、列名、ORDER BY子句需白名单校验在面试中遇到“MyBatis中#和$的区别”时能讲出LIKE场景是最加分的。模糊查询如果用like %#{name}%在SQL Server里可行但MySQL中会被当成字符串加法所以concat更通用。4.4 查询条件太多时的参数封装当模糊查询条件达到三四个时Controller方法入参越来越长建议用查询对象封装。这种方式在后期分页时尤其方便public class StudentQuery { private String name; private Integer clazzId; private Integer pageNo 1; private Integer pageSize 10; // getter/setter }Mapper接口写成ListStudent search(StudentQuery query);XML中既可以直接引用query.name也可以利用parameterType的短别名。查询对象的优势是能继续承载分页参数像limit #{offset}, #{pageSize}这种结构在SQL中可以直接使用#{}。偏移量计算可以放在Service层int offset (query.getPageNo() - 1) * query.getPageSize(); query.setOffset(offset);这样Controller的入参稳定Service对查询条件做预处理SQL层保持单一职责。5. 部署到Tomcat后的三个排错点与缓存边界5.1 连不上的数据库驱动、URL与网络SSM项目部署到Tomcat最常见的问题是启动时Cannot create PoolableConnectionFactory。先从三处检查jdbc.properties里的driverClass是否匹配MySQL版本MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x要换成com.mysql.cj.jdbc.Driverjdbc.url是否带时区例如jdbc:mysql://localhost:3306/db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiWEB-INF/lib里不能同时存在两个版本的mysql-connector-java jar。如果数据库服务器是独立机器还要检查MySQL的bind-address是否只绑定了127.0.0.1。Tomcat本身无法解决网络层问题但可以在启动日志中观察C3P0抛出的消息Access denied是认证问题Connection refused是网络或端口问题。5.2 MyBatis一级缓存与事务的配合MyBatis的一级缓存默认开启作用范围是SqlSession。原生MyBatis中每次sqlSession.selectList会开启新会话一级缓存意义不大。但整合Spring后SqlSessionTemplate会与Spring事务同步同一个事务内多次执行同一条SQL会命中一级缓存减少重复查询。判断缓存是否生效可以看SQL日志第二条相同查询没有打印SQL说明命中了。如果Service方法没有加Transactional每次Mapper调用都使用独立会话一级缓存不会跨方法生效。因此在学生管理这类简单项目中不要指望一级缓存节约数据库开销只应在明确的事务边界内利用它。二级缓存默认关闭开启后缓存由命名空间共享但需要处理关联表的数据同步如果学生和成绩分布在两个Mapper成绩更新后学生对应的缓存不会自动失效。这个项目的查询条件多不适合二级缓存保持默认关闭最安全。5.3 分页时把模糊查询条件带下去列表页一旦分页翻页链接必须保留查询字段。常见做法是把查询条件放在表单的隐藏域中或者构建带参URL。用JSP展示上一页、下一页时在HTML中要转义a href${pageContext.request.contextPath}/student/list?name${param.name}amp;pageNo${pageNo-1}上一页/a严格规范建议写amp;否则在XHTML下会被解析器警告。更好的方式是用一个隐藏input存储name提交表单时重新触发查询翻页按钮只修改hidden input的pageNo。这样避免在URL里暴露中文乱码。如果从Controller刷新重定向记得用RedirectAttributes.addAttribute把name和clazzId拼回URL否则翻页后条件丢失。这个思路同样适用于Spring Boot项目复制Mapper XML事务和拦截器配置迁移到配置类业务逻辑保持不变。本文还有配套的精品资源点击获取