文章速读
本文是一篇完整的 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.java

3.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: 6379

7.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.log

7.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 和前端的完整 URL
500 数据库字段缺失 / 空指针 查看完整 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,欢迎点赞 / 收藏 / 评论。
你在开发中遇到过哪些“离谱”的错误?评论区一起交流,我会定期回复。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐