文章速读
本文是一篇完整的 Java Web 全栈项目实战复盘。从项目构思、技术选型、用户模块开发、登录认证(MD5 + JWT + Redis)、接口联调,到最终部署在阿里云服务器,我完整走了一遍企业级 Web 应用的开发流程。
文中真实记录了 5 个最常见的“劝退级”错误:400 参数绑定异常、数据库字段非空导致的 500、MyBatis 映射失败、context-path 引起的 404、云服务器部署端口与进程问题。每个问题都附带了错误日志 → 原因分析 → 解决方案 → 验证方法,并提炼成一张【排错速查表】。
如果你是正在学习 SpringBoot 的初学者,这篇文章至少能帮你少踩 10 个坑。📌 目录
一、项目背景与开发心路
二、技术选型全景解读
2.1 技术栈表格
2.2 为什么选这套组合
三、项目结构与分层设计
3.1 代码目录树
3.2 核心数据库表设计
四、用户模块 CRUD:三个“新手必炸”的坑
坑 1:@RequestBody 用错导致 400
坑 2:数据库非空字段引发的 500
坑 3:MyBatis-Plus 驼峰映射失效
五、登录认证模块(MD5 + JWT + Redis)深度实现
5.1 为什么不只用 JWT?
5.2 完整流程图(Mermaid)
5.3 核心代码与关键注释
六、接口联调中的两个“隐藏杀手”
6.1 context-path 导致的 404
6.2 跨域问题(CORS)及 Nginx 解决方案
七、阿里云部署实战:从 0 到上线
7.1 环境准备与端口矩阵
7.2 数据库与 Redis 配置
7.3 Jar 包部署与后台启动
7.4 Nginx 反向代理配置
7.5 部署中踩的 2 个真实坑
八、面向高分的排错方法论总结
8.1 排错五步法
8.2 常见 Web 错误速查表
九、后续优化计划(含 Docker 方向)
十、致谢与互动
一、项目背景与开发心路
本学期《Web 应用项目开发》课程要求独立完成一个具有实际业务场景的全栈项目。我选择了“赛克能源管理系统”——一个面向企业内部能源数据管理的小型系统,核心模块包括:
用户管理(增删改查)
登录认证(JWT + Redis)
能耗数据填报与统计(接口预留,后续扩展)
整个项目耗时约20 天,从最初连
@Autowired和@Resource都分不清,到最后在阿里云上稳定运行。这期间我经历了无数次 400、404、500,也曾在凌晨两点盯着Field 'nick_name' doesn't have a default value怀疑人生。但正是这些错误,让我真正理解了 Web 开发的本质。本文不会只展示“最终正确代码”,而是完整还原每一次踩坑、定位、解决问题的全过程。
二、技术选型全景解读
2.1 技术栈表格
层级 技术 版本 核心作用 前端 HTML5 + CSS3 + ES6 — 基础页面与交互(后续可升级为 Vue) 后端框架 SpringBoot 2.7.6 简化配置、内嵌 Tomcat、自动装配 ORM MyBatis-Plus 3.5.5 单表操作零 SQL,分页插件友好 数据库 MySQL 8.0.31 持久化存储用户、能耗数据 缓存 Redis 5.0.14 存储 JWT 令牌,实现主动失效 安全 JWT + MD5 — 无状态认证 + 密码不可逆加密 部署 阿里云轻量 + 宝塔 + Nginx CentOS 7.9 生产环境部署与反向代理 工具 IDEA + Navicat + Postman + Git 2023 / 16 开发、调试、版本控制 2.2 为什么选这套组合
SpringBoot 2.7:稳定版本,文档丰富,社区活跃,同时兼容我后续想整合的 Spring Security。
MyBatis-Plus:相比原生 MyBatis 省去大量 XML 配置,且自带乐观锁、分页、代码生成器。
Redis + JWT:纯 JWT 无法做到服务端主动失效(比如踢用户下线),结合 Redis 缓存 token 后,登出时删除 Redis key 即可完美解决。
宝塔面板:对新手最友好的 Linux 运维工具,文件管理、端口放行、Nginx 配置都可可视化操作。
三、项目结构与分层设计
3.1 代码目录树
text
src/main/java/com/saike/ems/ ├── common │ ├── R.java // 统一响应结果封装 │ └── ResultCode.java // 业务状态码枚举 ├── config │ ├── CorsConfig.java // 跨域配置 │ ├── RedisConfig.java // Redis 序列化配置 │ └── MybatisPlusConfig.java // 分页插件配置 ├── controller │ └── UserController.java ├── service │ ├── IUserService.java │ └── impl │ └── UserServiceImpl.java ├── mapper │ └── UserMapper.java ├── entity │ └── User.java ├── dto │ └── LoginDTO.java ├── interceptor │ └── JwtInterceptor.java // 全局令牌校验 └── utils ├── Md5Util.java └── JwtUtil.java3.2 核心数据库表设计
sql
CREATE TABLE `user` ( `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID', `user_name` varchar(50) NOT NULL COMMENT '用户名', `password` char(32) NOT NULL COMMENT 'MD5加密后的密码', `nick_name` varchar(50) DEFAULT '赛克用户' COMMENT '昵称', `status` tinyint DEFAULT '1' COMMENT '状态:1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`), UNIQUE KEY `uk_user_name` (`user_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计说明:
password固定 32 位(MD5 输出长度)
nick_name设置了默认值,避免新增时因非空约束失败(这就是坑 2 的根源)用户名唯一索引,防止重复注册
四、用户模块 CRUD:三个“新手必炸”的坑
坑 1:@RequestBody 用错导致 400
现象
前端用 Postman 以x-www-form-urlencoded方式提交,后端报错:text
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing错误代码
java
@PostMapping("/add") public R addUser(@RequestBody User user) { // ❌ 错误 return userService.save(user) ? R.ok() : R.fail(); }分析
@RequestBody要求请求Content-Type: application/json,并且 body 中是 JSON 字符串。而表单提交的Content-Type是application/x-www-form-urlencoded,参数以key1=value1&key2=value2形式放在 body 中,两者完全不兼容。正确写法
java
@PostMapping("/add") public R addUser(User user) { // ✅ 直接使用实体接收 return userService.save(user) ? R.ok() : R.fail(); }或者显式接收参数
java
public R addUser(@RequestParam String userName, @RequestParam String password) { User user = new User(); user.setUserName(userName); user.setPassword(Md5Util.md5(password)); // ... }✅ 验证:Postman 中选择
x-www-form-urlencoded,输入字段后请求返回 200。坑 2:数据库非空字段引发的 500
现象
新增用户时控制台报错:text
### SQL: INSERT INTO user (user_name, password) VALUES ( ?, ? ) ### Cause: java.sql.SQLException: Field 'nick_name' doesn't have a default value分析
数据库nick_name字段定义为NOT NULL,且建表时未设置DEFAULT值。而实体类中nickName默认为null,MyBatis-Plus 生成的 INSERT 语句只包含user_name和password,导致数据库拒绝插入。解决方案
方案一(推荐):修改数据库,增加默认值
sql
ALTER TABLE `user` MODIFY COLUMN `nick_name` varchar(50) DEFAULT '默认昵称' NOT NULL;方案二(代码兜底):在业务层判断并设置默认值
java
if (user.getNickName() == null || user.getNickName().isEmpty()) { user.setNickName("赛克用户"); }✅ 验证:再次执行 INSERT,日志显示插入成功,数据库中出现新增记录。
坑 3:MyBatis-Plus 驼峰映射失效
现象
查询用户列表,返回的username字段全是null,但数据库中user_name有值。分析
MyBatis-Plus 默认开启mapUnderscoreToCamelCase,会将user_name映射为userName。
而我实体类中属性名是username(全小写),无法匹配。错误实体类
java
@TableName("user") public class User { private Integer userId; private String username; // ❌ 数据库是 user_name }正确实体类
java
@TableName("user") public class User { @TableId("user_id") private Integer userId; @TableField("user_name") // ✅ 显式映射 private String username; @TableField("nick_name") private String nickName; }或者统一命名风格
将实体类属性改为userName,利用默认驼峰转换(推荐)。✅ 验证:查询接口返回数据中 username 正常显示。
五、登录认证模块(MD5 + JWT + Redis)深度实现
5.1 为什么不只用 JWT?
纯 JWT 的痛点:无法主动失效。用户修改密码或退出登录后,旧 token 在过期前仍然有效。
解决方案:服务端将 JWT 存入 Redis,key 为用户唯一标识(如 userId),value 为 token。
每次请求携带 token 时,不仅要校验 JWT 签名,还要校验 Redis 中的 token 是否存在且一致。5.2 完整流程图(Mermaid)
5.3 核心代码与关键注释
java
@PostMapping("/login") public R<String> login(@RequestBody LoginDTO loginDTO) { // 1. 查询用户(已写在service中) User user = userService.getByUserName(loginDTO.getUserName()); if (user == null) { return R.fail("用户名不存在"); } // 2. 密码校验(MD5) String encrypted = Md5Util.md5(loginDTO.getPassword()); if (!encrypted.equals(user.getPassword())) { return R.fail("密码错误"); } // 3. 生成 JWT Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getUserId()); claims.put("userName", user.getUserName()); String token = JwtUtil.generate(claims); // 4. 存储到 Redis(1小时过期) String redisKey = "token:user:" + user.getUserId(); stringRedisTemplate.opsForValue().set(redisKey, token, 1, TimeUnit.HOURS); return R.ok(token); }登出逻辑:删除 Redis key 即可实现主动失效。
java
@PostMapping("/logout") public R<String> logout(@RequestHeader("Authorization") String token) { // 解析token获取userId Integer userId = JwtUtil.getUserId(token); stringRedisTemplate.delete("token:user:" + userId); return R.ok("已退出"); }六、接口联调中的两个“隐藏杀手”
6.1 context-path 导致的 404
现象
前端请求/user/list返回 404,但后端明明写了@GetMapping("/user/list")。分析
application.yml中配置了server.servlet.context-path: /api,导致后端实际路径为/api/user/list。前端请求没有加/api。解决
统一规范:前端 axios 配置baseURL: '/api',所有请求自动加上前缀。6.2 跨域问题(CORS)及 Nginx 解决方案
开发阶段直接在 SpringBoot 中配置跨域过滤器:
java
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }生产环境更推荐用 Nginx 反向代理解决(见第七部分)。
七、阿里云部署实战:从 0 到上线
7.1 环境准备与端口矩阵
端口 服务 宝塔放行 阿里云安全组放行 80 Nginx ✅ ✅ 9763 SpringBoot Jar ✅ ✅ 3306 MySQL ❌ 仅本地 ❌ 6379 Redis ❌ 仅本地 ❌ 8888 宝塔面板 ✅ ✅ 7.2 数据库与 Redis 配置
在宝塔中创建数据库
ems,导入 SQL 文件。修改
application-prod.yml:yaml
spring: datasource: url: jdbc:mysql://localhost:3306/ems?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码 redis: host: localhost port: 63797.3 Jar 包部署与后台启动
bash
# 上传jar包到 /www/wwwroot/ems cd /www/wwwroot/ems # 停止旧进程(如果有) pkill -f ems-0.0.1-SNAPSHOT.jar # 后台启动 nohup java -jar ems-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > logs/ems.log 2>&1 & # 查看日志 tail -f logs/ems.log7.4 Nginx 反向代理配置
nginx
server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /www/wwwroot/ems-front; index index.html; } # 后端API代理(解决跨域) location /api/ { proxy_pass http://127.0.0.1:9763/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }7.5 部署中踩的 2 个真实坑
坑 1:阿里云安全组开了 9763,但宝塔防火墙没开 → 仍然无法访问 → 两边都要放行。
坑 2:多次部署后出现Address already in use→lsof -i:9763找到进程kill -9再重启。八、面向高分的排错方法论总结
8.1 排错五步法
步骤 行动 产出 1. 复现 用相同参数再次请求,记录完整错误信息 错误现象确定 2. 看日志 后端控制台 / tail -f server.log异常堆栈 + 错误行号 3. 定位 根据堆栈找到对应代码行 问题代码位置 4. 查证 搜索引擎 + 官方文档 + Stack Overflow 2~3 种候选方案 5. 验证 每次只改一个变量,测试通过后复盘 永久解决问题 8.2 常见 Web 错误速查表
HTTP 状态 常见原因 排查方向 400 参数类型不匹配 / @RequestBody用错检查请求头 Content-Type,对比后端参数注解 401 未认证或 Token 失效 查看 Redis 中 token 是否存在,检查 JWT 过期时间 404 路径错误 / context-path 遗漏 对比后端 @RequestMapping和前端的完整 URL500 数据库字段缺失 / 空指针 查看完整 SQL 日志,检查数据库非空约束 502 后端服务未启动 / 端口不通 ps -ef | grep java,检查防火墙九、后续优化计划(含 Docker 方向)
统一异常处理:
@RestControllerAdvice接管所有异常,返回规范格式。参数校验:引入
spring-boot-starter-validation,使用@NotNull等注解。Redis 验证优化:在拦截器中实现 token 校验,避免每个方法重复写。
容器化部署:编写
Dockerfile+docker-compose.yml,一键启动 MySQL、Redis、后端。dockerfile
# 示例 Dockerfile FROM openjdk:11-jre COPY target/ems-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]十、致谢与互动
这门课程让我从“纸上谈兵”走向真正的工程实践。
感谢老师每节课对 Web 底层原理的剖析,感谢室友在我通宵调 Bug 时给予的精神支持。如果本文帮你解决了一个实际 Bug,欢迎点赞 / 收藏 / 评论。
你在开发中遇到过哪些“离谱”的错误?评论区一起交流,我会定期回复。
赛克能源管理系统全栈开发复盘:从零完成 SpringBoot + MySQL + Redis 项目,踩遍 Web 经典坑后我总结了这些教训