☰
SSM+JSP健身房管理系统全解析:从数据库设计到部署避坑
2026/10/7 1:10:18 网站建设 项目流程

简介:这是一套基于SSM(Spring+SpringMVC+MyBatis)与JSP技术栈开发的健身房管理系统完整源码,主要面向Java初学者、课程设计及毕业设计人群,可帮助快速掌握企业级Web项目的前后端整合思路。压缩包共1277个文件,包含120个Java后端类、109个JSP页面、364个JavaScript脚本,以及CSS、图片、字体等前端静态资源,同时提供MySQL数据库脚本与MAVEN工程配置,整体大小仅15.74MB,目录结构清晰,便于按模块检索阅读。已有113人学习下载。源码不仅可直接运行,还展示了SSM三层架构的典型实现:控制层处理请求、业务层封装逻辑、持久层交互数据库;前端JSP配合JavaScript完成页面渲染与异步交互,适合框架实战练习、二次开发或作为毕业设计参考,有助于深入理解SSM整合流程、JSP语法及应用部署细节。

1. 这套 SSM+JSP 健身房管理系统:课程设计与企业实习都要用的那套东西

一位刚毕业的 Java 开发,面试官问他做过什么项目,他说“健身房管理系统”。十有八九就是这套 SSM+JSP 的框架组合——Spring 管对象、SpringMVC 收请求、MyBatis 操作数据库、JSP 负责把数据渲染成页面。它不新,但结构完整到能直接支撑一个中小型健身房的会员、课程、教练、器材预约等日常业务。

这套系统的意义在于:它几乎是 Java Web 后端入门最典型的一条完整链路。你不需要会微服务、不需要懂分布式,只要把 SSM 三层架构和 JSP 页面的数据流转跑通,就已经覆盖了后端开发日常 80% 的工作方式。本文从零把这套系统的数据库设计、分层代码、常见部署问题全部走一遍,按步骤能复现,按经验能避坑。

如果你正面临 Java 课程设计、毕业设计,或者是想从“只会写 Java SE 题”过渡到“能跑一个 Web 项目”,这套架构是性价比最高的选择。它不像 Spring Boot 那样把配置都帮你藏起来,反而让你把每个注解、每段 XML、每条 SQL 都看得清清楚楚——这才是面试时说得出口的东西。

2. 技术选型和目录结构:为什么 SSM+JSP 到今天还有人在用

2.1 SSM 三个框架各自管什么,以及它们的集成方式

很多新手对 SSM 的认知是一个模糊的整体,但写代码和面试时你必须能一句话说清每个框架的职责边界。

Spring 是容器,管理所有对象的创建和依赖关系。你的 Service、Mapper、Controller 在传统写法里都得自己 new,Spring 里通过<bean>配置或注解来托管,调用方只需要注入,不需要关心对象怎么创建。这套系统里,我们会对 Service 层加上事务控制,保证一次操作要么全成功要么全回滚。

SpringMVC 是前端控制器,负责接收 HTTP 请求、解析参数、调用 Service、再把返回值交给视图渲染。它的核心是 DispatcherServlet,所有请求先经过它再分发到不同的 Controller 方法。这套系统里,登录请求、会员列表请求、预约操作都是由它来路由。

MyBatis 是持久层框架,解决 Java 对象和数据库记录之间的映射。对比 JDBC 手写PreparedStatement,MyBatis 让你把 SQL 写在 XML 或注解里,参数自动绑定,结果集自动映射成对象。比如查出一条会员记录,自动变成Member对象,不需要一行行rs.getInt()。

三个框架的集成方式常见的是:web.xml 里配置 Spring 的 ContextLoaderListener 加载 Spring 根容器;SpringMVC 的 DispatcherServlet 加载自己的子容器;MyBatis 的 SqlSessionFactory 作为 Bean 放进 Spring 容器,Mapper 接口通过扫描注册成 Bean。这套配置看起来头大,但写一次以后所有项目都是同一个套路。

2.2 标准目录结构:Maven 工程怎么组织才不乱

我见过太多课程设计项目把所有类都塞在默认包下面,跑起来没问题,但检查代码的老师一眼就能看出问题。SSM 项目标准结构按 Maven 约定来,分为主代码目录和资源目录。

项目骨架大致是:

gym-manager/ ├── pom.xml ├── src/main/java │ └── com/gym │ ├── controller │ ├── service │ │ ├── impl │ ├── mapper │ ├── entity │ └── common ├── src/main/resources │ ├── jdbc.properties │ ├── spring-mybatis.xml │ └── mapper └── src/main/webapp ├── WEB-INF │ ├── web.xml │ └── jsp ├── static └── index.jsp

controller 放控制器,service 放接口、impl 放实现类,mapper 放 MyBatis 接口,entity 放实体类,common 放分页工具类、统一返回结果类等。resources 下放 Spring 配置和 MyBatis 的 mapper XML。webapp 下是 JSP 页面和静态资源。

这个结构不是硬性规定,但按这个约定去组织,团队成员互相看代码成本最低。特别是 mapper 接口和 mapper XML 的包名必须一致,不然 MyBatis 扫描会报绑定异常,这是后面要重点讲的坑。

2.3 数据库表设计:六张表怎么撑起完整业务

健身房管理系统核心围绕“人”和“活动”来建模:会员、教练是核心实体,课程是载体,预约是连接关系,公告是信息补充,管理员是后台入口。按这个思路拆出六张核心表。

表名关键字段作用
adminid, username, password, real_name后台登录账号
memberid, name, phone, gender, age, card_type, create_time会员基本信息与卡类型
trainerid, name, specialty, phone, hire_date教练档案
courseid, name, trainer_id, course_date, start_time, end_time, capacity课程排期
reservationid, member_id, course_id, reserve_time, status会员预约课程记录
announcementid, title, content, publish_time公告信息

会员表里的 card_type 可以存字符串如“月卡”“年卡”,也可以在后续迭代里拆成卡类型字典表,课程表的 trainer_id 关联教练表。预约表是典型的关联表,会员和课程是多对多,通过预约表解耦。

值得注意的细节是时间字段统一用datetime而不是varchar,否则后面做“统计本月新增会员数”这类 SQL 时会非常痛苦。性别字段用tinyint存储,页面显示时再映射成“男/女”,比直接用中文更规范。数据库字符集用utf8mb4,不然以后会员备注里存个 emoji 就插入报错。

3. 从零搭建项目:环境版本、配置文件和数据库初始化

3.1 环境选型和版本搭配:这一步定错会连锁翻车

SSM 项目对版本非常敏感,尤其是 Spring 和 MyBatis 之间的兼容性。常见的稳定搭配是 JDK 1.8、Maven 3.6 以上、Tomcat 8.5 或 9.0、MySQL 5.7。这套组合经过了大量项目的验证,网上遇到问题时能搜到的解决方案也最多。

pom.xml 里引入核心依赖的参考写法如下:

<properties> <spring.version>4.3.18.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <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>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <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> </dependencies>

注意几个参数的含义:Spring 的版本统一用 4.3.x,不要混用 4.x 和 5.x 的 jar 包;mybatis-spring是 Spring 整合 MyBatis 的桥接包,版本必须和 Spring 4.x 兼容;mysql 驱动用 5.1.47,如果本地是 MySQL 8.x 的话驱动要换成com.mysql.cj.jdbc.Driver并加时区参数。这些版本号不是我拍脑袋写的,每一组都是踩过坑之后沉淀下来的可运行组合。

3.2 web.xml 和 Spring 配置文件:请求是怎么分发到 Controller 的

配置从 web.xml 开始,它是整个 Web 应用的入口。它负责两件事:加载 Spring 根容器,以及配置 SpringMVC 的前端控制器。

<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mybatis.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

这里有一个容易理解错的地方:ContextLoaderListener加载的是根容器,主要管 Service、Mapper 这些业务对象;DispatcherServlet加载的是子容器,主要管 Controller。子容器能访问父容器的 Bean,反过来不行。所以如果你的 Controller 想注入 Service,SpringMVC 容器里找不到时会去父容器找,这是可行的。

实际开发中为了避免扫描重复,Spring 的配置只扫描 service、mapper、entity,SpringMVC 的配置只扫描 controller,两个容器各管各的,边界清晰。

3.3 SpringMVC 配置和 MyBatis 整合:连接池、事务和视图解析

spring-mybatis.xml 是整套配置的核心,负责数据源、MyBatis 的 SqlSessionFactory、Mapper 扫描和事务管理。

<!-- 读取数据库配置 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 数据源:C3P0 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialPoolSize" value="5"/> <property name="maxPoolSize" value="20"/> <property name="minPoolSize" value="1"/> <property name="acquireIncrement" value="2"/> </bean> <!-- 整合 MyBatis --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.gym.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.gym.mapper"/> </bean> <!-- 事务管理 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

这段配置里,mapperLocations指向 XML 文件的位置,MyBatis 启动时会把 XML 里的 SQL 和接口方法绑定起来,路径写错就会报绑定异常。typeAliasesPackage设置了实体类的别名,这样 mapper XML 的resultType可以直接写Member而不必写全限定名。MapperScannerConfigurer会自动扫描指定包下的接口,生成代理对象注册进 Spring 容器。

<!-- spring-mvc.xml --> <context:component-scan base-package="com.gym.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:default-servlet-handler/>

视图解析器的意思是:Controller 返回member/list,实际渲染的是/WEB-INF/jsp/member/list.jsp。把 JSP 放在 WEB-INF 下是为了防止用户直接通过 URL 访问页面文件,必须经过 Controller 跳转。最后两行是静态资源放行配置,CSS、JS、图片都要靠这两行才能正常加载。

jdbc.properties 文件里维护数据库连接信息,注意用户名和密码不要写死成 root 明文,至少用环境变量替换,比如${DB_PASSWORD}。

4. 核心功能代码实现:登录、会员管理、课程预约的完整链路

4.1 实体类和 Mapper 层:SQL 是怎么和对象互相转换的

先看 Member 实体类,注意字段和数据库列名的映射规则。

public class Member { private Integer id; private String name; private String phone; private Integer gender; private Integer age; private String cardType; private Date createTime; // getter/setter 省略 }

MyBatis 默认开启驼峰命名转换后,数据库的card_type列能自动映射到 Java 的cardType属性。这个开关需要在 mybatis 配置里开启:<setting name="mapUnderscoreToCamelCase" value="true"/>。如果不开启,要么给每列写resultMap,要么把 SQL 里都用别名写成和属性一致。

Mapper 接口的定义方式:

public interface MemberMapper { Member selectById(Integer id); List<Member> selectAll(); List<Member> selectPage(@Param("offset") int offset, @Param("pageSize") int pageSize); int insert(Member member); int update(Member member); int deleteById(Integer id); int countAll(); }

对应的 mapper XML:

<mapper namespace="com.gym.mapper.MemberMapper"> <select id="selectPage" resultType="Member"> SELECT id, name, phone, gender, age, card_type, create_time FROM member ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select> <insert id="insert" parameterType="Member" useGeneratedKeys="true" keyProperty="id"> INSERT INTO member(name, phone, gender, age, card_type, create_time) VALUES(#{name}, #{phone}, #{gender}, #{age}, #{cardType}, NOW()) </insert> </mapper>

解释一下几个关键点:Mapper 接口的@Param注解是为了让 XML 里能用#{offset}引用参数,不写的话 MyBatis 取不到名。useGeneratedKeys配合keyProperty,插入后数据库自增的 id 会自动回填到对象的 id 属性,这样新增完会员后可以直接拿到他 id 去给别的表用。parameterType不写也能通过反射推断,但建议写清楚,可读性更好,MyBatis 解析时也能少走弯路。

4.2 Service 层:事务只加在该加的地方

Service 层是业务逻辑所在,典型的一段代码是预约课程。这里涉及两个操作:插入预约记录,同时把课程的剩余名额减一。这两个操作必须在一个事务里,要么都成功要么都失败。

@Service public class ReservationServiceImpl implements ReservationService { @Autowired private ReservationMapper reservationMapper; @Autowired private CourseMapper courseMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean reserve(Reservation reservation) { // 1. 校验该会员是否已预约过同一节课 int count = reservationMapper.countByMemberAndCourse( reservation.getMemberId(), reservation.getCourseId()); if (count > 0) { throw new BusinessException("不能重复预约同一课程"); } // 2. 校验课程是否还有名额 Course course = courseMapper.selectById(reservation.getCourseId()); if (course.getCapacity() <= course.getReservedCount()) { throw new BusinessException("课程已满"); } // 3. 插入预约记录 reservationMapper.insert(reservation); // 4. 课程已预约人数 +1 courseMapper.incrementReservedCount(reservation.getCourseId()); return true; } }

事务注解@Transactional默认只回滚 RuntimeException,用rollbackFor = Exception.class的意思是所有异常都触发回滚。常见的一种错误是写了@Transactional但内部 catch 住了异常打日志,事务就不会回滚。因为 Spring 事务是通过 AOP 代理实现的,异常必须穿透到代理层才会触发回滚逻辑。这里捕获异常后再抛出即可,代码逻辑没问题,但要注意这个坑。

Service 层里尽量不出现HttpServletRequest、HttpServletResponse这类 Web 层对象,保持业务逻辑独立,将来从 JSP 换成前后端分离接口时 Service 不需要改。

4.3 Controller 层:参数接收和响应跳转

Controller 层是 MVC 里的 C,它的核心是接收前端参数、调用 Service、跳转视图。

@Controller @RequestMapping("/member") public class MemberController { @Autowired private MemberService memberService; @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { PageResult<Member> page = memberService.findPage(pageNum, pageSize); model.addAttribute("page", page); return "member/list"; } @RequestMapping("/add") public String add(Member member) { memberService.add(member); return "redirect:/member/list"; } }

@RequestParam(defaultValue = "1")保证请求不带 pageNum 参数时不会报 400,兜底成第一页。Model 是 SpringMVC 提供的视图数据容器,addAttribute放入的数据可以在 JSP 里用${page.records}取出。redirect:前缀表示重定向,避免表单重复提交——用户刷新页面时不会再次提交上一次的表单数据。这是 PRG 模式(Post-Redirect-Get)的标准做法。

在 Java 面试题里经常被问到的@RequestBody、@ResponseBody,这套 JSP 项目里用得少,但你要知道它们存在的意义:@ResponseBody是把方法的返回值直接写成 JSON 响应给前端,而不是当成视图名去解析。如果面试官问你“SSM 项目怎么返回 JSON”,就是这两个注解配合实现。

4.4 JSP 页面:EL 表达式和 JSTL 渲染数据

JSP 页面里,最核心的工作是通过 EL 表达式把后台数据渲染到 HTML 上。这是 JSP 相比 Freemarker、Thymeleaf 的老派之处,但逻辑一致:模板变量替换。

以会员列表页的关键代码为例:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <table> <tr> <th>ID</th> <th>姓名</th> <th>手机号</th> <th>性别</th> <th>操作</th> </tr> <c:forEach items="${page.records}" var="m"> <tr> <td>${m.id}</td> <td>${m.name}</td> <td>${m.phone}</td> <td>${m.gender == 1 ? '男' : '女'}</td> <td> <a href="${pageContext.request.contextPath}/member/edit?id=${m.id}">编辑</a> <a href="${pageContext.request.contextPath}/member/delete?id=${m.id}" onclick="return confirm('确定删除吗?')">删除</a> </td> </tr> </c:forEach> </table> </body> </html>

JSP 两个容易出错的地方:pageContext.request.contextPath是获取当前项目上下文路径。项目部署时如果不是部署在 Tomcat 根路径,URL 必须带上下文路径。另一个是c:forEach的 items 必须是一个可遍历的集合,如果从后台传过来的对象不是 List 就会直接 500 报错。

4.5 分页查询:MySQL 的 LIMIT 参数怎么动态传

分页是列表页的标配需求,上面 MemberController 里引用了PageResult,这里把实现展开。

public class PageResult<T> { private List<T> records; private int pageNum; private int pageSize; private long total; private int totalPages; // getter/setter 省略 }

Service 实现里分页查询的写法:

public PageResult<Member> findPage(int pageNum, int pageSize) { int offset = (pageNum - 1) * pageSize; List<Member> records = memberMapper.selectPage(offset, pageSize); long total = memberMapper.countAll(); PageResult<Member> result = new PageResult<>(); result.setRecords(records); result.setPageNum(pageNum); result.setPageSize(pageSize); result.setTotal(total); result.setTotalPages((int) Math.ceil((double) total / pageSize)); return result; }

这里的offset计算是一个典型边界点。pageNum = 1时 offset 为 0,LIMIT 0, 10取第一页,这个推导要自己验算一遍,很多新人第 2 页拿到的是第 1 页重复数据,就是因为 offset 写成了pageNum * pageSize。

JSP 页面里还需要翻页导航,用 JSTL 实现:

<c:if test="${page.pageNum > 1}"> <a href="?pageNum=${page.pageNum - 1}&pageSize=${page.pageSize}">上一页</a> </c:if> <c:if test="${page.pageNum < page.totalPages}"> <a href="?pageNum=${page.pageNum + 1}&pageSize=${page.pageSize}">下一页</a> </c:if>

分页还有另一种常见用法是 PageHelper 插件,只需在 MyBatis 配置里添加一个拦截器,查询前调用PageHelper.startPage(pageNum, pageSize),后面的 SQL 自动追加 LIMIT。但用在课程设计里,手写分页反而更能体现功底,面试时问到你也能把 LIMIT 参数的计算逻辑说清楚。

5. SSM+JSP 项目避坑记:五个高频问题从现象到解决

以下都是实际调试中反复出现的问题,每一条都值得背下来。写代码时多花十分钟检查这些点,能省下后面一整天的排查时间。

5.1 启动报错:Failed to configure a DataSource

现象:Tomcat 启动时直接失败,控制台提示Failed to configure a DataSource: 'url' attribute is not specified。

原因:项目里引入了 spring-boot-starter-jdbc 依赖,Spring Boot 自动配置机制尝试创建数据源,但找不到数据库连接配置。SSM 项目里出现这个通常是因为 pom.xml 混入了 Spring Boot 相关依赖。

解决:检查依赖树,去掉所有 spring-boot-starter 开头的依赖。SSM 项目的数据源是自己声明 Bean 的,不需要 Spring Boot 自动配置。如果一定要保留某些工具类,用<exclusions>排除掉 auto-configuration 相关依赖。

5.2 请求报 404:HTTP 状态 404 的前端控制器映射坑

现象:访问/member/list返回 404,Tomcat 控制台无任何异常日志。

原因:SpringMVC 的<url-pattern>配置成了*.do或者/以外的路径。.do风格是 Struts 遗留习惯,如果写成了<url-pattern>*.do</url-pattern>,请求路径不带.do后缀自然匹配不到。

解决:把 DispatcherServlet 的 url-pattern 改成/,让它处理所有请求。改完后静态资源访问会失效,同时配置<mvc:default-servlet-handler/>,SpringMVC 会先把请求交给默认 Servlet 处理静态资源,处理不了的才进 Controller。

5.3 Invalid bound statement (not found):Mapper 接口和 XML 对不上

现象:浏览器访问任何调用到某个 Mapper 的接口都会报Invalid bound statement (not found),但接口类存在。

原因:MyBatis 扫描 mapper XML 时找不到对应的 XML 文件。常见原因有两个:一是 mapper XML 里的 namespace 写错了接口全限定名;二是 XML 没被编译到 classes 目录,Maven 默认只把src/main/resources下的文件打包进 classpath,如果你把 XML 放在src/main/java下面,默认会被忽略。

解决:第一,检查 XML 的 namespace 是否和接口全限定名完全一致;第二,把 XML 文件移到src/main/resources/mapper/目录下,或者在 pom.xml 里配置 resources 包含**/*.xml:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>

这个配置的意思是:虽然 XML 和 .java 在同一个目录,构建时也把 XML 文件复制到输出目录。

5.4 插入中文变成问号或乱码

现象:页面上新增的会员姓名在数据库里变成???,或者从数据库查出来显示乱码。

原因:乱码几乎都是字符集不统一链路上的某一环断了。链路是「JSP 页面编码 → Tomcat 请求解码 → MySQL 连接字符集 → MySQL 表字符集」。最常见的是 MySQL 连接 URL 没带字符集参数,以及 Tomcat 的 GET 请求默认用 ISO-8859-1 解码。

解决:三步到位。数据库连接 URL 加上?useUnicode=true&characterEncoding=utf8;表创建时指定DEFAULT CHARSET=utf8mb4;配置一个 CharacterEncodingFilter 拦截所有请求强制转 UTF-8,这个方法最有效。也许你习惯在每个 JSP 里写pageEncoding="UTF-8",我一般建议做的是写完之后,再抓一次包确认 Content-Type 里 charset 是 utf-8 还是 ISO-8859-1——这一步很重要,它负责的是配置文件没有覆盖到的漏网之鱼。

5.5 JSON 序列化问题:日期变成时间戳或格式不对

现象:Controller 返回一个含Date字段对象,前端拿到的是类似1612345678912的毫秒值。

原因:没有配置日期格式化,Jackson 默认把java.util.Date序列化成时间戳。

解决:在日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),并确保该字段有 getter。如果用的是 fastjson,则用@JSONField(format = "yyyy-MM-dd HH:mm:ss")。

这里额外提醒一个隐藏问题:timezone不配的话,数据库的 datetime 值可能被当成 UTC 时间解析,显示出来比当地时间慢 8 小时。这是“网络热词 jsp个人信息展示页面”背后经常被问到的一个细节——不仅是展示页面,所有涉及时间字段的序列化都躲不开这个规则。

6. 部署上线到服务器:打包、验证方法、MySQL 参数调到能用

本地跑通只是前半段,真正落地到服务器有几个容易翻车的地方,说一下打包部署和上线后的验证方法。

先讲打包。直接用 Maven 的 package 命令。

mvn clean package

打完包后在 target 目录会生成gym-manager.war,把 war 文件放进 Tomcat 的 webapps 目录,启动 Tomcat 后它会自动解压部署。需要注意:如果你的项目里用了 Lombok,打出来的 war 包依赖里必须带上它,否则线上环境报错java.lang.NoClassDefFoundError,本地开发不报是因为 IDE 插件替你处理了。

接着检查 MySQL 最大连接数,默认 151 在并发稍稍上来时就是瓶颈。登录到 MySQL 执行:

SHOW VARIABLES LIKE 'max_connections'; SET GLOBAL max_connections = 500;

这里注意SET GLOBAL只在当前实例生效,重启后还是要改配置文件里的max_connections。数据库连接池 C3P0 的maxPoolSize不要超过 MySQL 的max_connections,留出 10% 余量给其他客户端,这个参数调大不会让系统变快,反而可能让连接池抢占 MySQL 连接资源导致别人的查询排队。

上线后的验证不要只打开页面看有没有 404。列一个检查清单,按顺序过一遍:

检查项命令或操作预期结果
应用状态curl -I http://ip:8080/gym-manager/返回 200
数据库连接登录页面输入 admin 账号能进后台
日志无 ERRORtail -f logs/catalina.out无红色异常
静态资源加载浏览器 F12 看 Console无 404 资源
时区正确新增一个会员录入时间与本地时间一致

还有一个细节值得单独说:JSP 文件修改后不需要重启 Tomcat,但生产环境默认 JSP 是预编译的,修改后需要重新编译。如果线上发现改了页面内容但没生效,检查 Tomcat 的 conf/web.xml 里 servlet 的 development 参数是否为 true。开发阶段要保持 true,部署上线可以设成 false 节省 CPU,但意味着你得重新部署才能改前端页面。

最后说一点个人习惯。这套系统我前前后后帮人改过不下五个版本,每次验收时被追问最多的问题排在第一位的是“你的密码存的是明文吗”。哪怕只是一个课程设计,哪怕系统只在学校内网演示,我也建议至少用 BCrypt 存密码。第二多的追问是“你的 SQL 有防注入吗”。MyBatis 里#{}是预编译参数占位,安全;${}是字符串拼接,会有注入风险。凡是排序字段、表名这类无法用占位符的地方,用白名单校验后再拼接,不要把请求参数直接塞进去。这两条做上,答辩时就有底气了。

希望你把这套结构吃透后,不只是会复制粘贴,而是能从数据库表设计一路讲到部署细节。如果文章里某段配置或某个参数和你本地环境不一致,多看看控制台报错再对照检查,配置类的问题最怕静下心来看日志。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询