03

1.数据库变量与实体类变量细节:

  • user_pic varchar(128) default ‘’ comment ‘头像’ 用户头像的完整性约束是varchar(128),这是因为用户头像会放在第三方服务器上
  • 数据库变量命名:下滑线
  • 实体类变量命名:小驼峰
  • 实体类中不提供setter\getter\toString方法,通过引入lombok依赖在编译的时候自动注入
    在pom引入后刷新
    <!--lombok依赖-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.30</version>
    </dependency>

红字处理:
maven clean与install
检查插件:
在这里插入图片描述
检查注解编译器
在这里插入图片描述
在pojo实体类添加注解@Data-红字则alt+enter
点击Maven编译在这里插入图片描述
target中中可以看到新增的方法
在这里插入图片描述

2.Result实体类
根据开发文档,需要返回json格式,SpringBoot会自动转换为json
引入02资料\04_综合案例资料\02_后台资料\03_实体类中的Result类到pojo
在这里插入图片描述
data泛型T,代表可以是一个字符串,也可以是一个对象
Result函数没有构造函数,所以会报错,添加lombok的注解@NoArgsConstructor无参数的构造方法
3.注册接口功能分析:
在这里插入图片描述
4.Controller
创建UserController类在Controller,创建UserService接口在Service下,在Service的impl下创建实现类UserServiceImpl,实现UserService接口,在mampper下创建UserMapper接口
在这里插入图片描述
技巧:在左侧项目的菜单中,找到 “Compact Middle Packages”(压缩空的中间包),取消勾选压缩空的中间软件包
在这里插入图片描述
先写controller:

@RestController
@RequestMapping("/user")
public class UsserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public Result Reigster(String username,String password){
        //查询用户
        User u = userService.findByUserName(username);
        if(u == null) {
            userService.register(username,password);
            return Result.success();

            //注册
        }else{
            //占用
            return Result.error("用户名已被占用");
        }


    }
}

5.Service
技巧:快速增加没有写的方法:在红字的方法处alt+enter
在这里插入图片描述
自动生成findByUserName方法
在这里插入图片描述
技巧:ctrl+alt点击接口,可以直接转到实现类
在这里插入图片描述
技巧alt+enter可以快速实现接口的方法:
在这里插入图片描述
添加@Service,把当前对象注册到容器中,就不会报错了
需要先加密导入md5工具类:02资料\04_综合案例资料\02_后台资料\02_工具类\Md5Util到utils(复制粘贴即可)

@Service
public class UserServiceImpl implements UserService {
    //需要再Mapper层查询
    @Autowired
    private UserMapper userMapper;
    @Override
    public User findByUserName(String username) {
        User u = userMapper.findByUserName(username);
        return u;
    }

    @Override
    public void register(String username, String passowrd) {
        //加密
        String md5String =  Md5Util.getMD5String(passowrd);

        //添加
        userMapper.add(username,md5String);

    }
}

6.Mapper
alt+enter,快速生成两个方法userMapper.findByUserName与userMapper.add

@Mapper
public interface UserMapper {
    //根据用户名查询用户
    @Select("select * from user where username = #{username}")
    User findByUserName(String username);
    //插入新用户
    @Insert("Insert into User (username,password,create_time,update_time)" +
            " values(#{username},#{password},now(),now())")
    //关于值,now()是数据库的now函数,是当前时间
    void add(String username, String password);
}

一种非常阳春的验证方式:
powshell:
curl -X POST “http://localhost:8080/user/register?username=now&password=123456”
返回:
{“timestamp”:“2026-05-18T01:47:20.979+00:00”,“status”:406,“error”:“Not Acceptable”,“path”:“/user/register”}
虽然是错误不过这是因为Result返回的类型不符合要求-没有添加@Data注解导致没有setter、getter、toString方法,所以无法返回合适的数据类型
在数据库中可以看见结果
在这里插入图片描述
正式地测试方式:postman,创建workspace
在这里插入图片描述
在这里插入图片描述
创建测试集
在这里插入图片描述
该项目直接导入02资料\04_综合案例资料\04_测试用例\用户相关接口.postman_collection
在这里插入图片描述
在这里插入图片描述
post方法请求数据在body里,使用的方式是urlencoded
在这里插入图片描述
在这里插入图片描述
也是406,因为Result实体没有添加@Data注解,导致没有setter、getter、toString方法,所以无法返回合适的数据类型

前端 Jackson HttpMessageConverter Controller 前端 Jackson HttpMessageConverter Controller 返回 Result 对象 调用 MappingJackson2HttpMessageConverter 通过 getter 方法读取属性 将属性转换为 JSON 字符串 返回 JSON {"code":200, "message":"成功", "data":...}

###################################

   //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);

在这里插入图片描述

1.lombok原理:

前端发送JSON数据

Controller接收
使用 setter 将JSON转为Java对象

Service处理业务
使用 getter 读取数据
使用 setter 修改数据

Mapper操作数据库
MyBatis: getter获取值拼SQL
setter赋值查询结果

JDBC传输SQL
纯驱动,不关心getter/setter

MySQL执行SQL

返回结果
MyBatis使用 setter
将数据库字段赋值给Java对象

Controller返回
使用 getter
将Java对象转为JSON

前端收到响应

getter、setter、toString只负责修改实体类的变量值,并不负责对接数据库,所以lombok相当针对每个实体类的变量提供一个获取修改或处理值的方法,不涉及数据库。

Lombok插件介入

编写代码阶段

User.java
@Data
public class User {
private String name;
}

点击编译(javac)

读取注解 @Data

在内存中修改抽象语法树
添加getter/setter等方法

真正的编译过程
将修改后的代码编译

User.class
包含了所有方法的字节码

关于@Data

  • 组合注解@Data // 等价于以下5个注解的组合
    @Getter
    @Setter
    @ToString // ← 包含toString
    @EqualsAndHashCode
    @RequiredArgsConstructor
  • 因此@Data注解有弊端,如会将所有实体都提供toString方法,toString把对象转换成字符串表示,如果滥用@Data就会出现无限递归

无限递归的触发

@Data 生成的内容

toString方法

User.toString 调用 orders

Order.toString 调用 user

// 错误示例:双向关联导致无限递归
@Data
public class User {
   private Long id;
   private String name;
   
   private List<Order> orders;  // 用户的所有订单
}

@Data
public class Order {
   private Long id;
   private BigDecimal amount;
   
   private User user;  // 订单所属用户
}

// 测试代码
public class Test {
   public static void main(String[] args) {
       User user = new User();
       Order order = new Order();
       
       user.setOrders(List.of(order));
       order.setUser(user);
       
       // 危险!会无限递归,最终导致 StackOverflowError
       System.out.println(user.toString());  
       // User(name=null, orders=[Order(amount=null, user=User(name=null, orders=[Order(...)]))])
       // ↑ 无限循环下去
   }
}

正确的做法双发都用@Data互相排除,或者单独使用

@Data
public class User {
    @ToString.Exclude
    private List<Order> orders;
}

@Data  
public class Order {
    @ToString.Exclude
    private User user;
}
  • @Data生成的getter的风险场景

🚨 风险场景1:JSON序列化(最危险!)

这是最真实、最常见的风险!

@Data
public class User {
    private String username;
    private String password;  // 敏感字段
}

@RestController
public class UserController {
    
    @GetMapping("/user/{id}")
    public Result getUser(@PathVariable Long id) {
        User user = userService.getById(id);
        // Spring MVC会自动将User对象转成JSON
        return Result.success(user);  // ← 危险!password会被序列化!
    }
}

返回的JSON:

{
  "success": true,
  "data": {
    "username": "zhangsan",
    "password": "123456"  // ← 密码直接暴露给前端!
  }
}

黑客不需要调用任何方法,只需要:

  1. 正常访问 /user/1 接口
  2. 从响应中直接读取password字段

🛡️ 解决方案

@Data
public class User {
    private String username;
    
    @JsonIgnore  // ← Jackson注解,序列化时忽略
    private String password;
}

// 或者使用DTO模式(更安全)
public class UserDTO {
    private String username;
    // 没有password字段
}

// Controller返回DTO
@GetMapping("/user/{id}")
public Result getUser(@PathVariable Long id) {
    User user = userService.getById(id);
    UserDTO dto = new UserDTO();
    dto.setUsername(user.getUsername());
    // password不会被复制
    return Result.success(dto);
}

🚨 风险场景2:反序列化攻击(更隐蔽)

如果你的应用有反序列化功能,黑客可能构造恶意数据:

// 危险的反序列化代码
@PostMapping("/import")
public void importUser(String userData) {
    // 从用户输入反序列化Java对象
    User user = (User) new ObjectInputStream(
        new ByteArrayInputStream(userData.getBytes())
    ).readObject();  // ← 危险!可能被构造恶意数据
}

🚨 风险场景3:日志泄露(最常见)

@Service
public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    public void login(String username, String password) {
        User user = userMapper.selectByUsername(username);
        
        // 不小心打印了整个对象
        log.info("用户信息:{}", user);  // ← password会出现在日志文件
        // 日志内容:User(username=zhangsan, password=123456, ...)
    }
}

黑客如果获得了日志文件访问权限,就能看到所有密码。

🚨 风险场景4:调试接口泄露

@RestController
public class DebugController {
    
    // 开发人员留下的调试接口
    @GetMapping("/debug/user/{id}")
    public User debugGetUser(@PathVariable Long id) {  // ← 危险
        // 直接返回完整User对象,包含password
        return userService.getById(id);
    }
}

🔬 真正的攻击方式:不是调用getter,而是利用框架特性

攻击场景示例

// 你的代码
@Data
public class User {
    private String username;
    private String password;
}

@RestController
public class LoginController {
    
    @PostMapping("/login")
    public Result login(@RequestBody User loginInfo) {
        // Spring自动将JSON的password字段通过setter赋值
        User user = userService.login(loginInfo.getUsername(), loginInfo.getPassword());
        
        // 如果登录成功,可能返回user对象
        if (user != null) {
            return Result.success(user);  // ← 危险!密码被返回
        }
    }
}

黑客的攻击步骤:

  1. 正常发送登录请求,密码随意
  2. 登录成功后,服务器返回了包含密码的User对象
  3. 黑客从响应中获得了别人的密码(如果数据库存储的是明文)

🛡️ 完整的安全策略

1. 使用DTO模式(最推荐)

// 实体类(数据库映射)
@Data
public class User {
    private Long id;
    private String username;
    private String password;  // 实体类保留
    private String email;
}

// 请求DTO(只包含允许输入的字段)
public class UserLoginRequest {
    private String username;
    private String password;  // 登录时需要
}

// 响应DTO(只包含允许输出的字段)
public class UserResponse {
    private Long id;
    private String username;
    private String email;
    // 没有password
    
    public static UserResponse from(User user) {
        UserResponse resp = new UserResponse();
        resp.setId(user.getId());
        resp.setUsername(user.getUsername());
        resp.setEmail(user.getEmail());
        return resp;
    }
}

// Controller
@PostMapping("/login")
public Result login(@RequestBody UserLoginRequest request) {
    User user = userService.login(request.getUsername(), request.getPassword());
    if (user != null) {
        return Result.success(UserResponse.from(user));  // 安全
    }
}

2. 使用注解控制序列化

@Data
public class User {
    private Long id;
    private String username;
    
    @JsonIgnore  // 永远不序列化
    private String password;
    
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)  // 只允许写入,不读取
    private String confirmPassword;
}

3. 密码加密存储(底线)

@Data
public class User {
    private String passwordHash;  // 存储哈希,不是明文
    
    public void setPassword(String plainPassword) {
        this.passwordHash = BCrypt.hashpw(plainPassword, BCrypt.gensalt());
    }
    
    public boolean checkPassword(String plainPassword) {
        return BCrypt.checkpw(plainPassword, this.passwordHash);
    }
    
    // 没有getPassword()方法!
}

📊 风险总结表

攻击方式 是否需要getter 风险等级 真实可能性
JSON序列化泄露 ❌ 不需要(框架自动调用) 🔴 高 非常常见
日志泄露 ❌ 不需要(toString自动调用) 🟡 中 常见
反序列化攻击 ⚠️ 间接需要 🟠 中高 较少见
调试接口泄露 ❌ 不需要 🟠 中高 常见
直接调用getter ✅ 需要 🟢 低 几乎不可能

🎯 最终结论

  1. 真正的风险是框架自动序列化 - Spring MVC、Jackson等会自动调用getter
  2. @Data确实增加了风险 - 因为它让所有字段都有了getter,包括敏感的
  3. 解决方案
    • 永远使用DTO分离内部实体和外部接口
    • 敏感字段添加@JsonIgnore
    • 密码必须加密存储
    • 不要在日志中打印完整对象

Lombok 注解:@NoArgsConstructor 和 @AllArgsConstructor
注解作用
这两个是 Lombok 库提供的注解,用于自动生成构造方法,减少样板代码。
注解 生成的构造方法 说明
@NoArgsConstructor 无参构造方法 生成一个没有参数的构造器
@AllArgsConstructor 全参构造方法 生成一个包含所有字段作为参数的构造器


  1. 关于接口文档

接口文档的编写分工

你看到的这份文档非常规范,但实际开发中是否要写、写到什么程度,取决于项目规模和协作模式

不同场景的实践

1. 个人项目/练手项目

不需要写这种正式文档 ❌

2. 小型团队(2-5人)

需要简化版文档 ✅
  • 用 Apifox/Postman 协作,自动生成文档
  • 或者在 Wiki/语雀 写个简单表格
  • 前后端口头沟通 + 代码注释

3. 中大型项目(5人以上/跨部门)

需要详细文档 ✅✅
  • 你发的这种格式是标配
  • 必须有:路径、方法、参数类型、是否必填、返回格式、错误码
  • 用于:并行开发、测试用例编写、新人上手、运维对接

谁来写?

文档内容

参与者

产品经理

前端

后端

测试

业务描述 - PM

接口路径/参数 - 后端

返回字段 - 后端+前端对齐

错误码 - 后端

示例数据 - 后端

内容 谁负责 说明
业务需求 产品经理 “需要一个用户注册功能”
接口路径/方法 后端 /user/register POST
参数定义 后端+前端 字段名、类型、是否必填
返回结构 后端+前端 统一响应格式(code/message/data)
错误码 后端 0成功、1失败、401未登录…
示例数据 后端 让前端有假数据可调试
维护更新 谁改谁更新 接口变了必须同步文档

实际开发中的几种模式

模式1:代码注解 + 自动生成(最流行)⭐

@RestController
@Api(tags = "用户管理")
public class UserController {
    
    @PostMapping("/user/register")
    @ApiOperation("用户注册")
    public Result register(@Valid @RequestBody UserRegisterDTO dto) {
        // ...
    }
}

配合 Swagger/Knife4j,访问一个网页就能看到完整文档,不需要手写 .md

模式2:Apifox/Postman 协作

后端在 Apifox 定义接口 → 生成文档链接 → 前端直接用
修改接口 → Apifox 自动同步 → 前端收到通知

一个工具解决:接口调试 + 文档生成 + 自动化测试

模式3:手写 Markdown(你发的这种)

适用于:

  • 项目要求文档归档(国企、政府项目)
  • 没有自动生成工具的环境
  • 作为设计评审的交付物

你需要写到什么程度?

你的情况 建议
个人练手项目 不用写,直接用 Postman 测
毕业设计 简单写一下,放在论文附录
面试项目 不用写,面试官看代码
团队协作开发 用 Swagger/Apifox,别手写
大厂正式项目 有规范模板,按团队要求来

3.泛型T
泛型代表该变量可以是多个类型

标识 含义
T Type(类型)
E Element(元素,常用于集合)
K Key(键)
V Value(值)
N Number(数值)
? 通配符(未知类型)
  • 优势:
    • 类型安全-编译时进行类型检查,避免运行时 ClassCastException
    • 消除强制转换
    • 代码复用-一套代码适配多种类型
  • 重要限制(类型擦除)
    Java 泛型是编译时特性,运行时会被擦除(Type Erasure):
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// list1.getClass() == list2.getClass()  // true,运行时都是 ArrayList

4.设计模式-统一响应结果封装

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }


5.@PostMapping注解

注解 作用 支持的HTTP方法
@RequestMapping 通用请求映射 GET、POST、PUT、DELETE、PATCH…(全部)
@PostMapping 专门映射POST请求 仅POST

补充说明: @PostMapping@RequestMapping(method = RequestMethod.POST) 的简写。


6.@service

一、基本概念

@Service 注解的本质,是告诉 Spring 容器:“这个类的实例由我来管理,你把它创建出来,放在池子里,别人要用的时候你给出去。”

没有 @Service

public class UserService {
    // 你 new UserService(),自己管
}

@Service

@Service
public class UserService {
    // Spring 帮你 new,帮你存,帮你给别的类用
}

为什么要这样做?
因为你的 Controller 需要调用 UserService。如果没有 Spring 容器,你要自己写:

UserService userService = new UserService();

如果一个项目里有 50 个地方要用 UserService,你就要写 50 次 new
如果 UserService 的构造方法改了,50 个地方都要改。

@Service + 依赖注入(@Autowired),Spring 统一管理,你只需要声明“我要用”,不用管“怎么创建”。


二、设计层面的解释(理解“容器”是什么)

Spring 容器可以理解成一个 HashMap

Map<String, Object> container = new HashMap<>();
// key: "userService", value: UserService 的实例

当你写上 @Service,Spring 在启动时会做三件事:

  1. 扫描:找到所有带 @Service@Controller@Repository@Component 的类
  2. 实例化:通过反射调用构造方法,创建一个对象
  3. 注册:把这个对象放进容器(HashMap 里)

之后,当你在 Controller 里写:

@Autowired
private UserService userService;

Spring 就会从那个 HashMap 里根据类型找到 UserService 的实例,赋值给你。

这就是“控制反转(IoC)”

  • 以前:你主动 new(你控制对象的创建)
  • 现在:Spring 创建好,你只管要(控制权交给容器)

三、为什么是 @Service 而不是别的?

Spring 提供了多个类似注解,本质上都一样,只是语义不同

注解 语义 使用场景
@Service 业务逻辑层 UserService、OrderService
@Controller 控制层/接口层 UserController
@Repository 数据访问层 UserMapper
@Component 通用组件 不属于以上三类的工具类

底层原理上,这四个注解完全等价——它们都会被 Spring 扫描并注册到容器。
区别只是给开发者看的,让你一眼就知道这个类是干什么的。

你可以用 @Component 代替 @Service,程序一样跑。但别人看代码时会困惑:“这个类是业务逻辑吗?为什么不用 @Service?”


四、优点

这样做的好处:

  1. 解耦:调用方不关心被调用方怎么创建
  2. 易测试:可以轻松替换成 mock 对象
  3. 统一管理:单例、作用域、生命周期都由容器控制

五、记忆

把 Spring 容器想象成一个 人才市场

  • @Service:你在人才市场注册了一份简历
  • @Autowired:企业(Controller)来人才市场说“我要一个 UserService 类型的人”
  • 人才市场:从库里找到那个人,派过去

没有人才市场,每个企业都要自己招聘、自己培训、自己发工资——耦合度高,重复造轮子。

这就是 @Service 存在的理由。


7.@Autowired

一、@Autowired 的基本概念

@Autowired 是 Spring 框架提供的依赖注入注解,它的作用是:告诉 Spring 容器,把这个字段或参数所需要的对象,自动找出来并赋值进去。
没有 @Autowired 时,你需要手动 new

@RestController
public class UserController {
    // 没有 @Autowired:自己 new
    private UserService userService = new UserService();
}

@Autowired 时,Spring 帮你注入:

@RestController
public class UserController {
    @Autowired
    private UserService userService;  // Spring 会自动把 UserService 的对象赋值进来
}

区别:你不需要写 new,不需要知道 UserService 怎么创建的,Spring 从容器里找出来给你。


二、@Autowired 的工作流程

@Service`,整个流程是:

1. 你在 UserService 上写 @Service
   ↓
   Spring 启动时,创建 UserService 对象,放到容器里

2. 你在 UserController 里写 @Autowired
   ↓
   Spring 启动时,扫描到 @Autowired,去容器里找 UserService 类型的对象
   ↓
   找到后,赋值给 userService 字段

3. 你可以直接调用 userService.xxx()

这就是“依赖注入”:UserController 依赖于 UserService,这个依赖不是你自己 new 的,而是 Spring 帮你“注入”进来的。


三、@Autowired 的三种用法

用法 示例 最常见
字段注入 @Autowired private UserService userService; ✅ 最常用
Setter 注入 @Autowired public void setUserService(UserService userService) {...} 较少
构造器注入 public UserController(UserService userService) {...} (可省略 @Autowired) 官方推荐

你目前看到的 BigEvent 项目里,大概率用的是字段注入(最简洁)。


四、@Autowired 的匹配规则(重要)

@Autowired 默认按类型去容器里找。

@Autowired
private UserService userService;

Spring 会问自己:“容器里有没有类型是 UserService 的对象?”

情况 结果
找到一个 ✅ 注入成功
找到多个(比如 UserService 有两个实现类) ❌ 报错,Spring 不知道选哪个
找不到 ❌ 报错

解决“找到多个”的问题:配合 @Qualifier("具体名字") 指定要哪一个。

@Autowired
@Qualifier("userServiceImpl1")
private UserService userService;

五、为什么有时候不用写 @Autowired

在较新的 Spring Boot 项目中,如果一个类只有一个构造方法,可以省略 @Autowired

@RestController
public class UserController {
    private final UserService userService;

    // 只有一个构造方法,@Autowired 可以省略
    public UserController(UserService userService) {
        this.userService = userService;
    }
}

这是 Spring 4.3+ 开始支持的特性,官方也推荐用构造器注入(因为字段注入有一些缺点,比如不方便做单元测试)。

但在 BigEvent 这类课程里,为了简洁,大概率还是直接用字段注入 + @Autowired


8.HTTP请求与Postman

常用HTTP请求方法中,数据放在哪里?

请求方法 数据通常放在 典型场景
GET URL 参数(query string) 搜索、筛选、分页
POST Body(请求体) 创建资源、登录、提交表单
PUT Body 更新整个资源
PATCH Body 部分更新资源
DELETE URL 参数(或 Body) 删除指定资源

POST 请求:Body 里的常见数据格式

在 Postman 中,Body 选项卡里可以选多种格式:

1. JSON(最常用)

Content-Type: application/json
{
  "username": "张三",
  "password": "123456",
  "email": "zhang@example.com"
}

2. x-www-form-urlencoded(表单提交)

Content-Type: application/x-www-form-urlencoded
username=张三&password=123456&email=zhang@example.com

3. form-data(含文件上传)

Content-Type: multipart/form-data

可以混合文本字段和文件

4. raw + XML / Text / HTML

自由格式,自定义 Content-Type

5. binary

直接上传二进制文件(图片、PDF等)


Postman 中如何操作

  1. 选择请求方法为 POST
  2. 输入 URL
  3. 点击 Body 选项卡
  4. 选择合适的格式(如 raw + JSON
  5. 输入数据内容
  6. 点击 Send

![示意] Body → raw → JSON → 粘贴你的JSON数据


为什么 POST 不用 URL 传数据?

  • 安全性:URL 会被记录在浏览器历史、服务器日志中
  • 长度限制:URL 长度有限(约 2KB-8KB),Body 可以很大(MB甚至GB)
  • 数据类型:Body 可以传 JSON、文件、二进制等,URL 只能传简单字符串
  • 语义:GET 用于获取数据(幂等),POST 用于提交/创建(不幂等)

如果你要:

  • 搜索商品(关键词=手机,价格<1000) → GET,放 URL
  • 注册新用户(用户名、密码、头像) → POST,放 Body(JSON 或 form-data)

更多推荐