黑马SpringBoot3+Vue3(实战篇)学习记录六:lombok及注释,接口文档、泛型、设计模式-统一响应结果封装、压缩空的中间软件包postman工具、SpringBootJson转换
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方法,所以无法返回合适的数据类型
###################################
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
1.lombok原理:
getter、setter、toString只负责修改实体类的变量值,并不负责对接数据库,所以lombok相当针对每个实体类的变量提供一个获取修改或处理值的方法,不涉及数据库。
关于@Data
- 组合注解@Data // 等价于以下5个注解的组合
@Getter
@Setter
@ToString // ← 包含toString
@EqualsAndHashCode
@RequiredArgsConstructor - 因此@Data注解有弊端,如会将所有实体都提供toString方法,toString把对象转换成字符串表示,如果滥用@Data就会出现无限递归
// 错误示例:双向关联导致无限递归
@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" // ← 密码直接暴露给前端!
}
}
黑客不需要调用任何方法,只需要:
- 正常访问
/user/1接口 - 从响应中直接读取
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); // ← 危险!密码被返回
}
}
}
黑客的攻击步骤:
- 正常发送登录请求,密码随意
- 登录成功后,服务器返回了包含密码的User对象
- 黑客从响应中获得了别人的密码(如果数据库存储的是明文)
🛡️ 完整的安全策略
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 | ✅ 需要 | 🟢 低 | 几乎不可能 |
🎯 最终结论
- 真正的风险是框架自动序列化 - Spring MVC、Jackson等会自动调用getter
- @Data确实增加了风险 - 因为它让所有字段都有了getter,包括敏感的
- 解决方案:
- 永远使用DTO分离内部实体和外部接口
- 敏感字段添加
@JsonIgnore - 密码必须加密存储
- 不要在日志中打印完整对象
Lombok 注解:@NoArgsConstructor 和 @AllArgsConstructor
注解作用
这两个是 Lombok 库提供的注解,用于自动生成构造方法,减少样板代码。
注解 生成的构造方法 说明
@NoArgsConstructor 无参构造方法 生成一个没有参数的构造器
@AllArgsConstructor 全参构造方法 生成一个包含所有字段作为参数的构造器
- 关于接口文档
接口文档的编写分工
你看到的这份文档非常规范,但实际开发中是否要写、写到什么程度,取决于项目规模和协作模式。
不同场景的实践
1. 个人项目/练手项目
不需要写这种正式文档 ❌
2. 小型团队(2-5人)
需要简化版文档 ✅
- 用 Apifox/Postman 协作,自动生成文档
- 或者在 Wiki/语雀 写个简单表格
- 前后端口头沟通 + 代码注释
3. 中大型项目(5人以上/跨部门)
需要详细文档 ✅✅
- 你发的这种格式是标配
- 必须有:路径、方法、参数类型、是否必填、返回格式、错误码
- 用于:并行开发、测试用例编写、新人上手、运维对接
谁来写?
| 内容 | 谁负责 | 说明 |
|---|---|---|
| 业务需求 | 产品经理 | “需要一个用户注册功能” |
| 接口路径/方法 | 后端 | /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 在启动时会做三件事:
- 扫描:找到所有带
@Service、@Controller、@Repository、@Component的类 - 实例化:通过反射调用构造方法,创建一个对象
- 注册:把这个对象放进容器(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?”
四、优点
这样做的好处:
- 解耦:调用方不关心被调用方怎么创建
- 易测试:可以轻松替换成 mock 对象
- 统一管理:单例、作用域、生命周期都由容器控制
五、记忆
把 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 中如何操作
- 选择请求方法为 POST
- 输入 URL
- 点击 Body 选项卡
- 选择合适的格式(如
raw+JSON) - 输入数据内容
- 点击 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)
更多推荐



所有评论(0)