简介:基于SSM框架的学生信息管理系统完整项目,含Java源码、配置及数据库文件,面向Java Web开发者、课程设计及毕业设计学生。系统覆盖学生信息管理、成绩管理、班级管理、用户权限管理、操作日志等模块,采用模块化设计,便于扩展与二次开发。压缩包共348个文件、约8.28MB,以53个Java源码、53个class字节码、24个JSP页面、24个jar依赖、40个CSS样式、10个JS脚本、4个XML配置及数据库文件为主体,图片素材占比较大,目录清晰。项目体现Spring IoC/AOP、SpringMVC请求分发、MyBatis持久化映射等核心技术,同时涵盖需求分析、数据库建模与接口设计思路,兼顾理论与实操,可作为信息系统分析与设计课程的配套参考。目前已有58人学习下载。
1. 学生信息管理系统为什么选用SSM框架
如果你下载过一个叫学生信息管理系统.zip的作业包,大概率会发现里面是 SSM 框架:Spring 管对象、SpringMVC 管请求、MyBatis 管 SQL。这几乎是 Java Web 岗位面试里出现频率最高的组合。这个系统虽然看起来只是学生表的增删改查,但要把这三个框架真正拧在一起,涉及 Maven 依赖、数据源、事务、分页、JSON 响应和部署配置。下面我按自己搭这类项目时的顺序,把每一步的关键命令、参数和容易卡住的地方过一遍。
2. SSM框架的工程骨架与Maven依赖配置
SSM 项目第一关不是写业务,而是先把工程立起来。我一般会跳过 IDE 的向导,直接建一个 Maven 目录,这样后面换机器或者用命令行打包都方便。下面这套结构可以原样抄走,包名按你自己的公司域名替换即可。
2.1 Maven 目录结构与 pom.xml 里的六组依赖
ssm-student ├── pom.xml └── src/main ├── java/com/example/student │ ├── controller │ ├── service │ ├── dao │ └── entity ├── resources │ ├── mybatis │ ├── spring │ └── jdbc.properties └── webapp ├── WEB-INF │ ├── web.xml │ └── views └── staticcontroller放 SpringMVC 的处理器,service放业务接口和实现,dao放 MyBatis 的 Mapper 接口,entity放和表对应的 POJO。resources/mybatis下放mybatis-config.xml和 Mapper XML,resources/spring放 Spring 与 SpringMVC 的配置,webapp/WEB-INF/views放 JSP。注意WEB-INF下的页面不能直接通过 URL 访问,必须经过控制器转发,这样权限控制才有意义。
pom.xml是第一个容易踩坑的地方。这里给出一份可以直接用的依赖集合:
<properties> <spring.version>5.3.39</spring.version> <mybatis.version>3.5.16</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.23</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.3</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency> </dependencies>这里我特意避开了 Spring Boot,因为 SSM 面试往往要求你答出DispatcherServlet、ContextLoaderListener这些组件,直接上 Boot 会把这些细节都藏起来。mybatis-spring是整合的关键,它负责把 MyBatis 的SqlSessionFactory交给 Spring 管理。druid作为连接池,mysql-connector-j是驱动,pagehelper用于分页,jackson-databind用于处理 JSON。注意 MySQL 8.x 的驱动类名已经变成com.mysql.cj.jdbc.Driver,如果沿用旧配置会直接报 ClassNotFound。
版本号最好锁定,不要用LATEST或RELEASE。Spring 5.3.x 是当前 SSM 项目的主流基线,MyBatis 3.5.x 与 PageHelper 5.3.x 搭配成熟,升级到 PageHelper 6.x 时需要注意jsqlparser依赖变化。各依赖的用途可以对照下面这张表:
| 依赖 | 作用 | 关键注意点 |
|---|---|---|
| spring-webmvc | MVC 核心 | 会传递引入 spring-context、spring-aop |
| spring-jdbc | 数据源与事务支持 | 需要@Transactional时必须有 |
| mybatis-spring | 桥接 MyBatis 与 Spring | 依赖 spring-jdbc |
| druid | 连接池 | 配置文件用druid.*前缀 |
| pagehelper | 物理分页 | 需要配置拦截器 |
| jackson-databind | JSON 序列化 | 版本 2.15+ 修复了多个反序列化漏洞 |
2.2 web.xml 中 DispatcherServlet 与 ContextLoaderListener 的加载顺序
配置文件的加载顺序直接决定你的 Bean 是否重复创建。标准写法如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>ContextLoaderListener先启动,创建的是父容器,加载 Service、DAO、数据源这些业务组件;DispatcherServlet随后启动,创建子容器,只加载 Controller。如果两边都用<context:component-scan base-package="com.example.student"/>扫描全部包,Controller 会被注册两份,事务代理也可能失效。常见的做法是:applicationContext.xml扫描service和dao,springmvc.xml只扫描controller。这个约定要刻在脑子里。
<url-pattern>/</url-pattern>表示所有请求先进 DispatcherServlet,这样静态资源 css/js 也会被拦截。配合<mvc:default-servlet-handler/>或<mvc:resources>放行。如果用了 JSP,它本身不经过 DispatcherServlet,所以不需要特殊处理。
2.3 springmvc.xml 中的组件扫描、注解驱动和视图解析器
springmvc.xml至少要有这三样:
<context:component-scan base-package="com.example.student.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean><mvc:annotation-driven/>会注册RequestMappingHandlerAdapter,同时自动配置 JSON 消息转换器,前提是 classpath 里有 Jackson。没有这一行,Controller 方法上的@ResponseBody会不起作用。视图解析器把return "student/list"解析成/WEB-INF/views/student/list.jsp。
到这里,SSM 的骨架已经立住了。
3. 学生信息管理系统的数据层:表结构与MyBatis映射
数据层是 SSM 里最容易被忽略但最容易出问题的地方。很多人把精力放在控制器上,等到跑起来发现class_name字段始终拿不到,才意识到映射没有做对。下面从建表开始,一步步把 MyBatis 的数据源和 Mapper 配好。
3.1 学生表的结构设计与字段选型
先看 SQL 脚本:
CREATE TABLE student ( id BIGINT AUTO_INCREMENT PRIMARY KEY, stu_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT '1-男 0-女', age INT, class_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_no (stu_no), KEY idx_class_name (class_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个结构参考了绝大多数学生信息管理系统的基础需求。stu_no是学号,加唯一索引防止重复建档;class_name是班级名称,因为查询场景经常按班级筛选,给它一个普通索引;create_time用数据库默认时间,避免 Java 端到处new Date()。年龄用INT够用,性别用TINYINT比CHAR(2)更省空间,也方便扩展其他取值。
字段与 Java 属性的对应关系如下:
| 数据库字段 | Java 属性 | 类型映射 |
|---|---|---|
| id | id | Long |
| stu_no | stuNo | String |
| name | name | String |
| gender | gender | Integer |
| class_name | className | String |
| create_time | createTime | Date |
这里最大的坑是stu_no和class_name。MyBatis 默认的是列名直接赋值给同名字段,stuNo和stu_no不一样,所以必须在全局配置里开启驼峰映射。
3.2 数据源、MyBatis 配置与 Mapper 扫描
定义jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm_student?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456characterEncoding=utf8是中文不乱传的基础。如果漏掉,页面传进来的姓名存进数据库会变成问号。serverTimezone在 MySQL 8.x 下必须设置,否则时间类型报错。
然后在applicationContext.xml中装配数据源和 SqlSessionFactory:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.student.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>DruidDataSource的init-method和destroy-method对应连接池初始化和关闭,这两个必须写。MapperScannerConfigurer会自动扫描com.example.student.dao下的接口,并为它们生成代理对象,之后在 Service 里直接@Autowired注入即可。注意这里的属性是sqlSessionFactoryBeanName,不是sqlSessionFactory,写错了会在启动时提示循环依赖。
mybatis-config.xml只需要两条关键设置:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="SLF4J"/> </settings> </configuration>mapUnderscoreToCamelCase开启后,stu_no会自动映射到stuNo,create_time映射到createTime。logImpl建议用 SLF4J,方便后续在日志里看 SQL。
3.3 StudentMapper 接口与 XML 中的动态 SQL
接口定义如下:
public interface StudentMapper { List<Student> selectByCondition(@Param("name") String name, @Param("offset") int offset, @Param("limit") int limit); long countByCondition(@Param("name") String name); Student selectById(Long id); int insert(Student student); int update(Student student); int deleteById(Long id); }对应的StudentMapper.xml放在resources/mybatis/mapper/下:
<mapper namespace="com.example.student.dao.StudentMapper"> <select id="selectByCondition" resultType="com.example.student.entity.Student"> SELECT id, stu_no, name, gender, age, class_name, phone, email, create_time FROM student <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> </where> ORDER BY id DESC LIMIT #{offset}, #{limit} </select> <select id="countByCondition" resultType="long"> SELECT COUNT(*) FROM student <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> </where> </select> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO student (stu_no, name, gender, age, class_name, phone, email) VALUES (#{stuNo}, #{name}, #{gender}, #{age}, #{className}, #{phone}, #{email}) </insert> <update id="update"> UPDATE student <set> <if test="name != null">name = #{name},</if> <if test="phone != null">phone = #{phone},</if> <if test="email != null">email = #{email},</if> </set> WHERE id = #{id} </update> <delete id="deleteById"> DELETE FROM student WHERE id = #{id} </delete> </mapper><where>标签会自动处理AND前缀,避免条件为空时多出多余的WHERE AND。#{name}预编译参数,不会产生 SQL 注入。CONCAT('%', #{name}, '%')比直接写'%${name}%'安全得多,后者走字符串拼接,一旦 name 里有单引号就会炸。useGeneratedKeys和keyProperty用于回填自增主键,新增之后student.getId()可以直接拿到数据库生成的 id。update里的<set>标签只更新非空字段,但如果所有字段都传 null,SQL 会变成UPDATE student SET WHERE id=?,这是业务层需要避免的。
4. 业务层与控制器:学生增删改查和分页接口怎么写
数据层准备好后,接下来把 Service 和 Controller 串起来。很多初学者会在 Controller 里直接调 Mapper,短期能跑,但项目里一旦有多个角色都要查学生,SQL 就散得到处都是。我更习惯 Service 持有业务规则,Controller 只做参数解析和视图选择。
4.1 Service 层接口与事务边界
先定接口:
public interface StudentService { PageResult<Student> page(int pageNum, int pageSize, String keyword); Student getById(Long id); void create(Student student); void update(Student student); void delete(Long id); }实现类:
@Service @Transactional(rollbackFor = Exception.class) public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public PageResult<Student> page(int pageNum, int pageSize, String keyword) { int offset = (pageNum - 1) * pageSize; List<Student> rows = studentMapper.selectByCondition(keyword.trim(), offset, pageSize); long total = studentMapper.countByCondition(keyword.trim()); return new PageResult<>(rows, total, pageNum, pageSize); } @Override public void create(Student student) { if (studentMapper.selectByStuNo(student.getStuNo()) != null) { throw new RuntimeException("学号已存在"); } studentMapper.insert(student); } @Override public void delete(Long id) { studentMapper.deleteById(id); } }注意几点:@Transactional加在类级别,意味着所有公有方法都被事务拦截,并且rollbackFor必须指定Exception.class,否则只在 RuntimeException 时回滚。create里先查学号再插入,这一段是典型的业务规则,放在 Controller 里会污染流程。Service 返回的PageResult是自己写的 POJO,包含rows、total、pageNum、pageSize四个字段,前端可以据此计算总页数。
4.2 分页查询:手写 LIMIT 还是 PageHelper
刚才的page方法用的是手写LIMIT,这在小系统里最直白。offset从 0 开始,所以(pageNum - 1) * pageSize是必须的。如果前端传的页码从 1 开始,而后端忘了减 1,第二页会跳掉一条数据。
PageHelper 是更省事的替代方案,用法是:
PageHelper.startPage(pageNum, pageSize); Page<Student> page = (Page<Student>) studentMapper.selectAll();startPage之后的第一个查询会被拦截并自动拼上 LIMIT,同时把总条数放进返回的 Page 对象。但它的失效边界很多:比如startPage和查询之间不能有其他 SQL、不能有线程复用等。两种方式的取舍可以看下表:
| 方案 | SQL 是否可见 | 分页插件依赖 | 多表查询适配 |
|---|---|---|---|
| 手写 LIMIT | 清晰可控 | 无 | 需要自己拼 count |
| PageHelper | 看不到完整 SQL | 有 | 自动 count,但复杂 SQL 可能出错 |
对于只有一个简单查询的作业系统,我建议手写 LIMIT,踩的坑更少。如果决定用 PageHelper,需要在mybatis-config.xml里添加插件:
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> <property name="reasonable" value="true"/> </plugin> </plugins>helperDialect指定方言,reasonable为 true 时,页码超过总数会自动落回最后一页,而不是报错。
4.3 Controller 返回视图与返回 JSON 的写法
Controller 里最容易被问到的两个注解是@RequestParam和@ResponseBody。下面这个控制器既有页面跳转,也有 JSON 接口:
@Controller @RequestMapping("/student") public class StudentController { @Autowired private StudentService studentService; @GetMapping("/list") public String list(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(defaultValue = "") String keyword, Model model) { model.addAttribute("page", studentService.page(pageNum, pageSize, keyword)); return "student/list"; } @GetMapping("/{id}") @ResponseBody public Student detail(@PathVariable Long id) { return studentService.getById(id); } @PostMapping("/save") @ResponseBody public Result save(@RequestBody Student student) { studentService.create(student); return Result.success(); } }list方法返回字符串,SpringMVC 结合视图解析器渲染 JSP。detail和save返回 POJO,Jackson 会把对象序列化成 JSON。这里/save接收的是 JSON 字符串,所以用@RequestBody解析并绑定到Student,如果前端用普通表单提交,则应该去掉@RequestBody,让 SpringMVC 按照表单字段名绑定。注意@PathVariable Long id是从 URL 路径里取值,对应GET /student/123。
分页参数放在@RequestParam上,默认值必须写全,否则第一次打开页面会报 MissingServletRequestParameterException。keyword 参数在 Service 里已经做了trim(),前端传空格也不会干扰查询。
5. 前端页面与SSM交互:表单提交、Ajax与参数绑定
到了前端这一步,常见项目有两种路线:一是用 JSP + JSTL 渲染服务端页面,二是用 HTML + Ajax 调 JSON 接口。SSM 传统作业多数是前者,但近几年很多同学会把后端写成纯 API,前端用原生 JS。这一章两种都讲,重点在参数怎么对得上。
在动手写页面前,先想清楚交互方式。下表列出两种方式的区别:
| 交互方式 | 适用场景 | 数据格式 | 后端配合 |
|---|---|---|---|
| JSP + JSTL | 简单查询、服务端渲染 | Model 数据 | 返回视图名 |
| Ajax + JSON | 异步操作、前后端分离 | JSON | 返回 @ResponseBody |
5.1 用 JSP + JSTL 渲染学生列表
列表页student/list.jsp的核心片段:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <table> <thead> <tr><th>学号</th><th>姓名</th><th>班级</th><th>入学时间</th></tr> </thead> <tbody> <c:forEach items="${page.rows}" var="s"> <tr> <td>${s.stuNo}</td> <td>${s.name}</td> <td>${s.className}</td> <td><fmt:formatDate value="${s.createTime}" pattern="yyyy-MM-dd"/></td> </tr> </c:forEach> </tbody> </table>${page.rows}会调用PageResult.getRows(),EL 表达式不需要写 get 前缀。${s.stuNo}对应Student.getStuNo()。如果后端返回的是createTime这种 Date 类型,直接输出会变成Fri Jun 06 2025 09:30:00 GMT,用<fmt:formatDate>格式化成业务需要的样式。页面顶部必须写taglib,否则<c:forEach>会被当成普通标签显示在页面上。
5.2 表单提交路径与 @ModelAttribute 绑定
新增学生的表单:
<form action="${pageContext.request.contextPath}/student/save" method="post"> <input type="text" name="stuNo" required/> <input type="text" name="name" required/> <input type="text" name="className"/> <button type="submit">保存</button> </form>SpringMVC 在没有@RequestBody时,会根据表单的name属性匹配Student的同名字段,这一过程被称为数据绑定。这里的关键是name="className"要和实体字段一致,后端不需要写任何解析代码。如果实体里是class_name,表单里也要写class_name,但实际上 Java 属性是className,所以表单必须用驼峰。把表单字段和实体字段对齐是这类页面最常见的问题。
还有一点容易被忽略:表单的action一定要写${pageContext.request.contextPath},否则在 Tomcat 上部署时,URL 缺少项目路径会造成 404。很多学生信息管理系统在本地 IDE 里能跑,打包部署后全挂,就是因为这里写死了/student/save。
5.3 Ajax 删除与 JSON 响应乱码
删除操作用 Ajax 更舒服:
fetch(contextPath + "/student/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: id }) }).then(res => res.json()).then(data => { if (data.success) location.reload(); });后端对应这样写:
@PostMapping("/delete") @ResponseBody public Result delete(@RequestBody Student student) { studentService.delete(student.getId()); return Result.success(); }如果不用@RequestBody,直接表单提交id=1也可以,方法签名改成public Result delete(Long id)即可。使用 JSON 的好处是复杂对象不会受表单字符集影响,但坏处是Content-Type必须写对。另一个坑是响应乱码,解决方法是在springmvc.xml中配置:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="defaultCharset" value="UTF-8"/> </bean> </mvc:message-converters> </mvc:annotation-driven>这个配置只对 String 输出有效,JSON 输出由 Jackson 的Utf8JsonGenerator处理,一般不会乱码。如果页面显示中文乱码,第一步先看响应头Content-Type是否包含charset=UTF-8,第二步检查 JSP 页面编码,第三步检查数据库连接地址。我自己遇到过一种情况:页面过滤器把编码强制转成 ISO-8859-1,导致 Ajax 请求里的中文全部变成问号,最后在 web.xml 里加了一个CharacterEncodingFilter才解决。
6. 部署SSM学生信息管理系统时最容易翻车的三个配置
最后这部分,我把部署到 Tomcat 后最常见的三个问题连同检查顺序列出来,你可以照着排查。
6.1 打 WAR 包并关闭测试
mvn clean package -DskipTests-DskipTests跳过测试编译,比-Dmaven.test.skip=true更实际,后者连测试类都不编译,可能导致打包失败信息被掩盖。输出的 WAR 包在target/下,复制到 Tomcat 的webapps/目录,启动后通过http://localhost:8080/ssm-student/student/list访问。
6.2 启动后的三个高频现象
| 现象 | 检查点 | 解决方向 |
|---|---|---|
| 启动即报 Filter 或 Servlet 类找不到 | Tomcat 版本与 javax 依赖是否匹配 | Spring 5.x 使用 javax;Spring 6 要求 jakarta,确认没有混用 |
| 页面 404 | DispatcherServlet 的 url-pattern 是否拦截了 JSP | JSP 不会被/拦截,但/student/list需要正确携带 context-path |
| 中文乱码 | jdbc.url 和页面编码 | 检查characterEncoding=utf8和过滤器 CharacterEncodingFilter |
最后留一个我每次都会检查的技巧:在mybatis-config.xml里打开 SQL 输出,看到实际执行的 SQL 再确认问题。
<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>如果搭了日志框架,把org.mybatis的日志级别设为 DEBUG。看 SQL 日志时,你能直接看到LIMIT是否拼接、参数是否匹配、WHERE条件是否正确,这比盯着异常栈猜快得多。
本文还有配套的精品资源,点击获取