简介:面向Java开发学习者的毕业设计/课程设计源码资源,基于SSM(Spring+SpringMVC+MyBatis)+JSP+MySQL实现龙腾公司员工信息管理系统,覆盖员工、部门、职位、薪资、培训、考勤六大业务模块,包含前后端完整源码、数据库脚本和配套说明文档,适合需要参考企业管理类项目或快速搭建管理后台的读者。资源压缩包共891个文件,体积约10.47MB,以Java源码、JSP页面、JS脚本、CSS样式和PNG图片居多,并含SQL数据库脚本与XML配置文件,目录结构清晰,便于按模块定位代码。目前已有63人学习,适合用于毕业设计选题参考或SSM框架综合实训。读者可从中获取完整可运行的项目原型,理解员工信息管理场景下的用户增删改查、部门职位维护、薪资条管理、培训记录与上下班打卡等功能的具体实现,也能借助说明文档梳理部署流程、初始化数据库和二次开发思路。
1. 员工信息管理系统的技术选型,为什么不是 Spring Boot
当“龙腾公司员工信息管理系统”这类毕业设计标题出现在面前,大多数人的第一反应是“怎么又是个 SSM”。坦白说,如果放到 2025 年的企业级开发环境,Spring Boot + Vue 才是主流组合;但在高校毕设语境下,SSM(Spring + SpringMVC + MyBatis)+ JSP + MySQL 依然是一个非常有教学价值的方案。它的价值不在于“新”,而在于把 Java Web 开发中最核心的手动配置、请求流转、ORM 映射和页面渲染全部暴露在明面上,适合用来检验对 Servlet 容器、Spring IoC 容器、SpringMVC 处理器映射、MyBatis 代理机制的理解程度。本文顺着“龙腾公司员工信息管理系统”这个项目的常见实现路径,把从数据库建模到 SSH 部署的完整链路拆开讲透。无论你是正在做同类毕设的学生,还是要帮团队快速搭一套老旧技术栈内部系统的工程师,都可以按本文的步骤直接落地。
2. SSM 架构下员工管理系统的分层设计与数据库建模
2.1 三层架构的职责边界与包结构规划
SSM 项目最常见的分层方式是 Controller-Service-Dao 三层,加上 JSP 视图层,正好对应 SpringMVC 的@Controller、Spring 的@Service、MyBatis 的Mapper三层组件。员工信息管理系统涉及的典型模块包括:员工档案维护、部门管理、薪资记录、用户登录与权限控制。这些模块的业务逻辑都不算复杂,但分层不清晰的代码往往会在学期末答辩时被问到“为什么把业务逻辑写在了 Controller 里”这类问题。
我会这样规划包结构:
com.longteng ├── controller # SpringMVC 控制层 │ ├── EmployeeController.java │ ├── DepartmentController.java │ └── UserController.java ├── service # 业务接口与实现 │ ├── EmployeeService.java │ ├── impl │ │ └── EmployeeServiceImpl.java ├── dao # MyBatis Mapper 接口 │ ├── EmployeeMapper.java │ └── EmployeeMapper.xml ├── pojo # 实体类 │ ├── Employee.java │ └── Department.java ├── interceptor # 登录拦截器 │ └── LoginInterceptor.java └── util # 工具类 └── PageBean.java分层的逻辑是:Controller 只负责参数接收、调用 Service、返回视图或数据;Service 处理业务规则(比如入职日期不能早于出生日期、部门员工数统计);Dao 只做 SQL 交互。pojo里的实体类字段与数据库表字段一一对应,util下放分页工具和字符串校验工具。这种结构的好处是,答辩时可以清晰地说出“数据流向是 JSP → Controller → Service → Dao → MySQL”,每一步改动的影响范围是可预期的。
2.2 数据库表设计:员工表、部门表、用户表的关系与字段约束
员工信息管理系统的核心表通常有三张:employee(员工表)、department(部门表)、admin(登录用户表)。部门与员工是典型的一对多关系,用一个外键dept_id关联;员工与用户表则不做物理外键关联,因为登录账号可能属于管理员而不是普通员工,两张表在逻辑上独立即可。
以下是建表 SQL 的常用写法:
CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '部门ID', `dept_name` varchar(50) NOT NULL COMMENT '部门名称', `dept_manager` varchar(20) DEFAULT NULL COMMENT '部门负责人', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表'; CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '员工ID', `emp_no` varchar(20) NOT NULL COMMENT '工号', `name` varchar(30) NOT NULL COMMENT '姓名', `gender` char(2) DEFAULT NULL COMMENT '性别', `birthday` date DEFAULT NULL COMMENT '出生日期', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `dept_id` int(11) DEFAULT NULL COMMENT '所属部门ID', `position` varchar(30) DEFAULT NULL COMMENT '职位', `hire_date` date DEFAULT NULL COMMENT '入职日期', `status` tinyint(1) DEFAULT '1' COMMENT '1在职 0离职', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`), KEY `idx_dept_id` (`dept_id`), CONSTRAINT `fk_emp_dept` FOREIGN KEY (`dept_id`) REFERENCES `department` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表'; CREATE TABLE `admin` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(30) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT '密码', `real_name` varchar(30) DEFAULT NULL COMMENT '真实姓名', `role` varchar(10) DEFAULT 'ADMIN' COMMENT '角色', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';这个建模有几点值得展开说明。工号emp_no设置为唯一键是为了防止重复录入;status字段不是直接 DELETE 而是软删除,保留离职记录;外键fk_emp_dept加在dept_id上,保证了不会插入一个不存在的部门。字符集统一用utf8mb4而不是utf8,是为了避免后期要存某些特殊符号时出现 “Incorrect string value” 错误。在 MyBatis 的映射中,dept_id会作为一个普通字段映射到 Employee 实体,而为了在列表中直接显示部门名称,需要在 SQL 中用JOIN department查出dept_name,并在实体里加一个冗余字段deptName。
2.3 实体类与 MyBatis 映射文件的字段对应策略
实体类写法上没有太多取巧空间,但有一个容易被忽略的坑:如果数据库字段是下划线风格(dept_id),而实体字段是驼峰风格(deptId),那么必须在mybatis-config.xml中开启驼峰映射,否则查询出来这个字段永远为 null。配置代码如下:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>对应的 Employee 实体类核心字段如下:
public class Employee { private Integer id; private String empNo; private String name; private String gender; private Date birthday; private String phone; private String email; private Integer deptId; private String deptName; // 关联查询用,非数据库字段 private String position; private Date hireDate; private Integer status; private String remark; // getter/setter 省略 }deptName不是数据库表字段,它只在联表查询时被 MyBatis 自动填充。Mapper XML 中对应的联表查询写法是:
<select id="selectEmployeeList" resultType="com.longteng.pojo.Employee"> SELECT e.*, d.dept_name AS deptName FROM employee e LEFT JOIN department d ON e.dept_id = d.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> </where> ORDER BY e.id DESC </select>这段 SQL 的精髓在<where>标签里的两个<if>,这是 MyBatis 动态 SQL 最常用的场景。name和deptId是传入的查询条件对象属性,如果某个条件为空,对应的 SQL 片段不会拼接进语句,避免出现AND开头的语法错误。注意LIKE CONCAT('%', #{name}, '%')使用了 CONCAT 函数拼接,而不是直接在 XML 写'%${name}%',后者会有 SQL 注入风险,而且$取值不经过预编译。知道这个区别,在答辩时被问到“MyBatis 怎么防注入”就能给出一个很扎实的回答。
3. 从零搭建 SSM 员工信息管理系统的配置文件骨架
3.1 pom.xml 依赖清单与版本兼容性选择
假设使用 Maven 管理依赖,第一步是把最关键的框架版本定下来。SSM 组合中常见的坑是 Spring 版本与 JDK 版本不兼容。JDK 1.8 环境推荐用 Spring 5.0.x 或 4.3.x,MyBatis 用 3.5.x,MyBatis-Spring 用 2.0.x,MySQL 驱动用 5.1.49(对应 MySQL 5.7)或 8.0.x(对应 MySQL 8.0)。
pom.xml 核心依赖如下:
<!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.15.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.0.15.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.15.RELEASE</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.5</version> </dependency> <!-- MySQL 驱动,版本与数据库对应 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> <!-- JSP 相关 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>依赖版本不是随便选的。Spring 5.0.15 对 JDK 8 的支持最稳定,如果用到 JDK 11 以上则建议升到 5.2.x 或直接换 Spring Boot。Druid 连接池在这个组合里比 c3p0 更有优势的地方在于内置监控页面和 SQL 防火墙,可以作为一个加分项写进说明文档。javax.servlet-api的scope必须设置为provided,因为 Tomcat 容器本身已经带了 Servlet 实现,重复引入会导致启动冲突。
3.2 applicationContext.xml、springmvc.xml、web.xml 三份配置的分工与写法
SSM 项目里配置文件是初学者最容易绕晕的点。核心逻辑是:applicationContext.xml管理 Service、Dao、数据源等业务层组件;springmvc.xml只管理 Controller 和视图解析器;web.xml负责把 Spring 容器和 SpringMVC 前端控制器装配进 Tomcat。
先看applicationContext.xml的关键部分:
<!-- 读取数据库配置 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 数据源:Druid --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <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> <!-- SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:com/longteng/dao/*.xml"/> </bean> <!-- Mapper 扫描 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.longteng.dao"/> </bean> <!-- 开启注解扫描,排除 Controller --> <context:component-scan base-package="com.longteng"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>这份配置里要特别关注mapperLocations的路径。它指定了 Mapper XML 文件的位置,如果 XML 和 Mapper 接口不在同一个包下或者路径写错,启动时会报 “Invalid bound statement (not found)” 错误。这是一个非常高频的启动报错,后面排错章节会专门讲。另外注意<context:component-scan>排除了@Controller注解,避免 Controller 被 Spring 容器重复加载。
springmvc.xml的配置相对简单:
<!-- Controller 扫描 --> <context:component-scan base-package="com.longteng.controller"/> <!-- 注解驱动 --> <mvc:annotation-driven/> <!-- 静态资源放行 --> <mvc:resources mapping="/static/**" location="/static/"/> <!-- JSP 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>视图解析器的prefix指向/WEB-INF/views/,这意味着所有 JSP 页面放在WEB-INF下,用户不能通过 URL 直接访问,只能经过 Controller 转发。这是 JSP 项目里一个重要的安全设计,答辨时可以主动提出来。mvc:resources放行静态资源是因为 SpringMVC 的DispatcherServlet默认会拦截所有请求,如果不放行,CSS、JS、图片会被拦截导致页面样式丢失。
web.xml需要完成三件事:配置 Spring 容器监听器、配置 SpringMVC 前端控制器、配置字符编码过滤器。编码过滤器必须放在最前面,否则 POST 请求的中文参数会在到达 Controller 前就乱码。完整的web.xml如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <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: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>DispatcherServlet的<url-pattern>用/而不是*.do,这是两种截然不同的风格。用/表示所有请求都进 SpringMVC,配合mvc:resources放行静态资源;用*.do则只需要在 SpringMVC 里配<mvc:default-servlet-handler/>。我一般倾向用/,因为 URL 更干净,不需要带后缀,也更贴近 Spring Boot 的Rest 风格。
3.3 jdbc.properties 与 mybatis-config.xml 的参数说明
jdbc.properties是五行的文件,但每一行都对应一个运行期参数:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/longteng_emp?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456useSSL=false一定要加,否则 MySQL 5.7 以上版本会在连接时打出 SSL 警告并可能影响连接速度。characterEncoding=utf8是中文不乱码的第一道保障,和 web.xml 里的编码过滤器共同作用,保证“数据库 → JDBC → Java 字符串 → JSP 页面”这条链路上编码一致。
mybatis-config.xml里除了之前提到的驼峰映射,还可以配置控制台 SQL 日志输出:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>logImpl=STDOUT_LOGGING让 MyBatis 直接把执行的 SQL 和参数打印到控制台,这在调试时极有用。等系统稳定后可以把这一行去掉,避免生产环境日志刷屏。这个配置在答辩演示时也可以有意保留,它是“通过日志可以实时看到 MyBatis 执行了哪条 SQL、传了什么参数”的直接证据。
4. 员工管理核心功能实现:登录拦截、增删改查与分页
4.1 登录校验与拦截器配置:控制未登录用户的越权访问
员工信息管理系统首先应该有一个“入口关卡”。我用 SpringMVC 的HandlerInterceptor实现登录拦截,拦截所有/**请求,放行登录接口和静态资源。拦截器代码如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/static/")) { return true; } // 检查 Session 中是否存在登录用户 Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,跳转登录页 response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }在springmvc.xml中注册拦截器:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.longteng.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这段代码的价值体现在两个细节上。第一,/login页面本身不需要登录就能访问,所以要在<mvc:exclude-mapping>里排除掉;第二,response.sendRedirect带上了request.getContextPath(),这样部署在非根路径下(比如http://localhost:8080/emp/)时跳转不会丢前缀。很多同学在跳转时直接写"login",部署到带项目名的路径上会 404,这是很典型的低级错误。
4.2 EmployeeController 的 CRUD 接口设计与参数绑定
员工管理的 Controller 核心方法包括:列表展示、跳转新增页、执行新增、跳转编辑页、执行更新、执行删除(软删除或物理删除)。下面是列表查询和新增的完整实现:
@Controller @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; @Autowired private DepartmentService departmentService; @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, String name, Integer deptId, Model model) { PageBean<Employee> page = employeeService.findPage(pageNum, pageSize, name, deptId); model.addAttribute("page", page); model.addAttribute("deptList", departmentService.findAll()); model.addAttribute("name", name); model.addAttribute("deptId", deptId); return "employee/list"; } @RequestMapping("/add") public String add(Employee employee) { employee.setStatus(1); // 默认在职 int rows = employeeService.addEmployee(employee); if (rows > 0) { return "redirect:/employee/list"; } return "error"; } }@RequestParam(defaultValue = "1")处理了“用户没有传页码”的情况。add方法接收的Employee对象由 SpringMVC 自动完成参数绑定,表单里name="empNo"的输入框会直接映射到employee.getEmpNo()。这个方法结束后没有跳转 JSP,而是redirect到列表页,这样可以避免表单重复提交——这是 PRG(Post-Redirect-Get)模式在 JSP 时代最简单的落地方式。
Service 层实现分页查询时,最直接的做法是手写 LIMIT 计算:
public PageBean<Employee> findPage(int pageNum, int pageSize, String name, Integer deptId) { int offset = (pageNum - 1) * pageSize; List<Employee> list = employeeMapper.selectPage(offset, pageSize, name, deptId); int total = employeeMapper.count(name, deptId); PageBean<Employee> pageBean = new PageBean<>(); pageBean.setList(list); pageBean.setTotal(total); pageBean.setPageNum(pageNum); pageBean.setPageSize(pageSize); pageBean.setTotalPage((total + pageSize - 1) / pageSize); return pageBean; }对应的 Mapper XML 需要两条 SQL,一条查当前页数据,一条查总数:
<select id="selectPage" resultType="com.longteng.pojo.Employee"> SELECT e.*, d.dept_name AS deptName FROM employee e LEFT JOIN department d ON e.dept_id = d.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> </where> ORDER BY e.id DESC LIMIT #{offset}, #{pageSize} </select> <select id="count" resultType="int"> SELECT COUNT(*) FROM employee <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND dept_id = #{deptId} </if> </where> </select>分页参数的传递逻辑是:Controller 拿到页面传来的pageNum,Service 把它换算成offset(偏移量),MyBatis 执行LIMIT offset, pageSize。这种手写分页的方式在数据量超过十万行时会出现深分页性能问题——比如查第 10000 页时,LIMIT 100000, 10依然要扫描前面 100000 行。毕业设计阶段一般不需要做深度优化,但如果想体现技术深度,可以在 Service 层或 Mapper 层做一次优化,常见做法是将LIMIT offset, pageSize改写为LIMIT pageSize OFFSET offset(这是 MySQL 8.0 的推荐写法),或者用“延迟关联”先查出 id 再回表。答辩时提到这一点,会明显拉开和其他人的差距。
4.3 JSP 列表页的 JSTL 渲染与分页导航提交技巧
JSP 页面是这套系统的“门面”。员工列表页的核心是<c:forEach>循环 +${}EL 表达式:
<%@ 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> <th>职位</th> <th>入职日期</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach items="${page.list}" var="emp"> <tr> <td>${emp.empNo}</td> <td>${emp.name}</td> <td>${emp.gender}</td> <td>${emp.deptName}</td> <td>${emp.position}</td> <td><fmt:formatDate value="${emp.hireDate}" pattern="yyyy-MM-dd"/></td> <td> <c:choose> <c:when test="${emp.status == 1}">在职</c:when> <c:otherwise>离职</c:otherwise> </c:choose> </td> <td> <a href="${pageContext.request.contextPath}/employee/toEdit?id=${emp.id}">编辑</a> <a href="${pageContext.request.contextPath}/employee/delete?id=${emp.id}" onclick="return confirm('确认删除该员工?')">删除</a> </td> </tr> </c:forEach> </tbody> </table>分页导航条的写法有一个常见的隐藏 bug:当搜索条件存在时,点击页码要保留之前的条件参数,所以分页链接不能只传pageNum,还必须把name和deptId带回去:
<c:forEach begin="1" end="${page.totalPage}" var="i"> <a href="${pageContext.request.contextPath}/employee/list?pageNum=${i}&name=${name}&deptId=${deptId}"> ${i} </a> </c:forEach>这里的${page.totalPage}是 PageBean 的属性,在 Service 层已经计算好。JSP 的 EL 表达式访问的是对象的 getter 方法,所以 page 对象里必须有getTotalPage()方法。另外注意${pageContext.request.contextPath}这个内置对象,它相当于 Java 代码里的request.getContextPath(),用来拼项目根路径,保证所有 a 标签的链接在部署时不会丢上下文。有的项目会偷懒直接写/employee/list,部署到根路径没问题,但如果有项目名就会 404,这是 JSP 项目最常见的前后端衔接坑。
5. 用 IDEA + Tomcat 跑通系统,从导入源码到浏览器验证
5.1 环境准备:JDK 1.8、MySQL 5.7、Tomcat 8.5 的组合与版本对应
“源码能运行”是这类标题最核心的诉求。在动手之前先把环境定住,能避免一半以上的报错。我建议的组合是:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | Spring 5.0.x 官方支持最高到 JDK 8 |
| MySQL | 5.7 | 5.1.49 驱动原生支持,避免 8.0 密码加密插件问题 |
| Tomcat | 8.5.x | 支持 Servlet 3.1,与javax.servlet-api3.1.0 匹配 |
| IDEA | 2020.3 及以上 | 社区版即可,但 Ultimate 对 Web 开发支持更好 |
MySQL 8.0 不是不能用,但要注意两个差异点:驱动必须换成com.mysql.cj.jdbc.Driver,jdbc.url里要加serverTimezone=Asia/Shanghai;另外 MySQL 8.0 默认的caching_sha2_password认证方式会让 5.1.49 驱动连接时报错,需要执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';。这类问题在毕业设计群里被问的次数非常多,如果在说明文档里写清这个坑,整个项目的完成度会高一个档次。
5.2 IDEA 中导入 Maven 项目并配置 Tomcat 的完整步骤
在 IDEA 中跑起来这个项目的步骤如下,每一步都有明确的验证点:
- IDEA 中选择
File -> New -> Project from Existing Sources,选中解压后的项目根目录,选择Maven类型导入。 - 打开
File -> Project Structure -> Project,确认Project SDK为 1.8,Language Level选 8。 - 展开右侧 Maven 面板,点击刷新按钮,等待依赖下载完。验证标准是
External Libraries中出现spring-webmvc-5.0.15.RELEASE等依赖。 - 打开
Run -> Edit Configurations,点击+选择Tomcat Server -> Local。 - 在
Server标签页选择 Tomcat 安装目录,Deployment标签页点击+ -> Artifact -> 项目名:war exploded,Application context填/longteng。 - 启动前检查 MySQL 是否已启动、
longteng_emp数据库是否已导入,jdbc.properties中的用户名密码是否与本地一致。 - 点击 Debug 按钮启动,等待控制台输出
Initializing Spring DispatcherServlet或Spring Context相关日志。
第 5 步是很多新手卡住的地方。war exploded是展开目录部署,比war包方式启动更快,修改 JSP 不需要重启整个 Tomcat,刷新页面就能看到效果。Application context是访问路径,填longteng访问地址就是http://localhost:8080/longteng/login。如果用的是内置了 Tomcat 的 IDEA 版本,Deployment里没有 Artifact 可选,这是因为项目没有添加到 Web 工程,需要在Project Structure -> Facets里手动添加 Web 支持。
启动后如果控制台出现Connected to server,但在浏览器访问一直转圈,多半是 8080 端口被占用。用下面命令查端口并杀进程:
netstat -ano | findstr 8080 taskkill /F /PID 进程号5.3 常见启动报错场景与对应排查方向
运行 SSM 项目时最常遇到的几个报错,我把现象、原因和排查点整理成一览表:
| 报错现象 | 根因 | 排查动作 |
|---|---|---|
Invalid bound statement (not found) | Mapper 接口与 XML 不匹配或mapperLocations路径配错 | 检查 Mapper XML 的namespace是否为接口全限定名,检查mapper-locations指向的路径 |
ClassNotFoundException: org.springframework.web.context.ContextLoaderListener | Spring-web 依赖缺失或 Maven 依赖未下载完整 | 检查 pom.xml 是否有spring-web,执行mvn clean后再启动 |
The type java.io.FileNotFoundException: path指向WEB-INF/views/xxx.jsp | 视图解析器的prefix路径不存在或文件名大小写不匹配 | 确认 JSP 文件确实放在src/main/webapp/WEB-INF/views/下 |
Table 'longteng_emp.employee' doesn't exist | 数据库名或表名不一致 | 检查jdbc.url中的库名与本地数据库名是否完全一致 |
Access denied for user 'root'@'localhost' | 数据库用户名或密码错误 | 核对jdbc.properties的账号密码,先用命令行mysql -uroot -p验证 |
java.sql.SQLException: Unknown character set: 'utf8mb4' | MySQL 5.5 及以下版本不支持 utf8mb4 | 确认 MySQL 版本不低于 5.6,或在建库时指定CHARACTER SET=utf8 |
Invalid bound statement是出现频率最高的报错,绝大多数原因是 Mapper 接口方法的id与 XML 里的<select>的id没有一一对应,或者是 XML 文件的路径没有打进编译输出目录。检查方法是在 IDEA 的target/classes目录下看是否生成了EmployeeMapper.xml,如果没有,就在 pom.xml 的<build>节点里加资源过滤配置,确保 XML 被 Maven 复制到 classes 目录。这个排查过程本身就能写成说明文档里的一个小节。
另外无论 IDA 控制台还是浏览器里显示 404 或 500,第一件事永远都是看 Tomcat 的catalina.log,它位于 Tomcat 安装目录下的logs文件夹中。Stack Trace 里的Caused by:部分才是真正需要关注的错误原因。
6. 离职员工软删除改造与 LIKE 查询索引失效的处理技巧
6.1 软删除改造:让系统保留员工历史档案
原项目如果用的是物理删除DELETE FROM employee WHERE id = ?,可以把它改成软删除,即用UPDATE employee SET status = 0 WHERE id = ?替代。这个改造本身只需要改一条 SQL,但连带影响列表查询和编辑功能。
改造后的查询语句要默认过滤掉已离职的员工,但提供按状态筛选的能力:
<select id="selectPage" resultType="com.longteng.pojo.Employee"> SELECT e.*, d.dept_name AS deptName FROM employee e LEFT JOIN department d ON e.dept_id = d.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> <if test="status != null"> AND e.status = #{status} </if> </where> ORDER BY e.id DESC LIMIT #{offset}, #{pageSize} </select>逻辑上,“离职”在业务里应该是一个状态切换操作,而不是删除操作。做这个改造后,可以在说明文档里写清楚:status=1为在职、status=0为离职,列表页默认不传 status 时显示全部,筛选在职时传status=1。这样员工离职之后,之前的薪资记录、部门归属信息依然可以被查询到,这在企业真实场景里很有意义。
6.2 员工姓名模糊查询在数据量增大时的索引失效场景
LIKE CONCAT('%', #{name}, '%')在 name 字段没有索引时是全表扫描,有索引时也会因为前导通配符%导致索引失效。MySQL 的 B+ 树索引支持最左前缀匹配,%张%这种模式用不上索引,只有张%这种前缀匹配才能命中索引。
一个折中策略是提供两种查询模式:精确查询走索引,模糊查询走全表扫描,由用户界面选择。对于员工管理系统这种规模的应用,员工数通常不会超过几千人,全表扫描的性能完全可以接受,重点是不要在查询条件的拼接上引入不必要的开销。如果一定要优化 LIKE 查询,最直接的做法是全文索引或搜索引擎方案,这对一个毕业设计来说是过度设计。我会在代码里保留模糊搜索能力,但会在 Mapper 注释里说明其性能和代价。
最后要说一个 JSP 项目特有的坑:修改employee/list.jsp后不需要重启 Tomcat,但不代表刷新一定有变化。IDEA 中部署war exploded时,修改 JSP 文件直接刷新浏览器即可生效;如果修改的是 Java 类,则需要用到 Tomcat 的Update resources按钮(位于 Debug 弹窗里)或重启服务。另外浏览器缓存可能让 CSS 修改“看起来没生效”,按Ctrl+F5强制刷新即可。这个细节写进说明文档的“常见问题”部分,实用性很高。
本文还有配套的精品资源,点击获取