作为面试后的复盘,本系列将对面试中出现的问题进行全方位拆解:不仅给出标准答案,更深入剖析背后的设计逻辑、实践误区和思考路径,同时结合表格对比、代码示例和分步推导,让每个知识点都能落地复用。无论你是准备面试的求职者,还是想夯实基础的开发同学,相信都能从中有所收获!
一、Spring Boot 依赖循环:问题本质、解决方案与底层原理
1.1 什么是依赖循环?
依赖循环(Circular Dependency)是 Spring 容器在创建 Bean 时,出现的 "相互依赖" 场景 ------ 简单说就是:Bean A 的创建需要注入 Bean B,而 Bean B 的创建又需要注入 Bean A,形成闭环导致容器无法正常初始化。
典型示例:
java
// Bean A
@Component
public class ServiceA {
private final ServiceB serviceB;
// 构造器注入ServiceB
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
// Bean B
@Component
public class ServiceB {
private final ServiceA serviceA;
// 构造器注入ServiceA
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
启动 Spring Boot 应用时,会抛出BeanCurrentlyInCreationException异常,提示 "Requested bean is currently in creation: Is there an unresolvable circular reference?"。
1.2 依赖循环产生的核心原理
要理解依赖循环,必须先掌握 Spring Bean 的生命周期核心流程(以单例 Bean 为例):
- 实例化(Instantiation):通过构造器创建 Bean 的实例(此时 Bean 仅完成对象创建,属性未赋值);
- 属性填充(Populate):注入依赖的 Bean(@Autowired、@Resource 等注解生效);
- 初始化(Initialization) :执行
@PostConstruct注解方法、实现InitializingBean接口的afterPropertiesSet方法等; - 销毁(Destruction):容器关闭时执行销毁逻辑。
依赖循环的本质是:Bean A 在 "属性填充" 阶段需要 Bean B,但 Bean B 此时正处于 "实例化" 阶段,尚未完成创建,导致相互等待。
举个生活类比:你要想拿到驾照(Bean A),需要先有身份证(Bean B);而你要办身份证(Bean B),又需要先有驾照(Bean A)------ 两个证件的办理相互依赖,最终陷入死循环。
1.3 依赖循环的 5 种解决方案(附实践对比)
Spring Boot 提供了多种解决依赖循环的方案,不同方案适用于不同场景,以下是最常用的 5 种方式,结合代码示例和优缺点对比:
| 解决方案 | 核心逻辑 | 代码示例 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 构造器注入改 Setter 注入 | 构造器注入会在 "实例化" 阶段依赖 Bean,Setter 注入延迟到 "属性填充" 阶段,允许循环依赖 | java @Component public class ServiceA { private ServiceB serviceB; @Autowired public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; } } @Component public class ServiceB { private ServiceA serviceA; @Autowired public void setServiceA(ServiceA serviceA) { this.serviceA = serviceA; } } |
大多数日常场景,非核心服务依赖 | 简单易用,无需额外配置 | 不能保证依赖注入的不可变性(可通过 final + 构造器注入解决,此方案会失去该特性) |
| @Lazy 注解(延迟初始化) | 对循环依赖的 Bean 进行延迟初始化,注入时先创建 "代理对象",实际使用时才初始化目标 Bean | java @Component public class ServiceA { private final ServiceB serviceB; @Autowired public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; } } @Component public class ServiceB { private final ServiceA serviceA; @Autowired public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; } } |
构造器注入场景,需要保持 final 特性 | 不改变注入方式,保持 Bean 不可变性 | 代理对象可能带来微小性能损耗,不适用于即时初始化场景 |
| @DependsOn 注解 | 强制指定 Bean 的初始化顺序,让其中一个 Bean 先完成初始化再创建另一个 Bean | java @Component@DependsOn("serviceB")//强制先初始化serviceB public class ServiceA { private final ServiceB serviceB; @Autowired public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } } @Component public class ServiceB { private final ServiceA serviceA; @Autowired public ServiceB(@Lazy ServiceA serviceA) { this.serviceA = serviceA; } } |
有明确初始化顺序要求的场景 | 可控制 Bean 创建顺序 | 强耦合初始化顺序,后续维护成本高 |
| 引入第三方中间件 | 拆分循环依赖,通过中间件(如 EventBus、消息队列)实现间接通信,解除直接依赖 | java public class BusinessEvent { private String message; // getter/setter ServiceA serviceA; @Component public class ServiceA { @Autowired private ApplicationEventPublisher publisher; public void doBusiness() { // 发送事件替代直接调用ServiceB publisher.publishEvent(new BusinessEvent("业务触发")); } } ServiceB serviceB; @Component public class ServiceB { @EventListener public void handleEvent(BusinessEvent event) { // 处理事件,间接响应ServiceA的调用 System.out.println("接收事件:" + event.getMessage()); } } } |
复杂业务场景,循环依赖涉及多个 Bean | 彻底解耦,符合高内聚低耦合原则 | 增加系统复杂度,需要维护事件和监听器 |
| @Scope("prototype") | 改变 Bean 的作用域(默认 singleton),原型 Bean 每次创建时重新实例化,避免循环引用 | java @Component @Scope("prototype") public class ServiceA { private final ServiceB serviceB; @Autowired public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } } @Component @Scope("prototype") public class ServiceB { private final ServiceA serviceA; @Autowired public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; } } |
非单例场景,Bean 无需全局共享 | 简单配置,无需修改注入逻辑 | 原型 Bean 每次创建新实例,可能导致性能问题,不适用于单例需求 |
1.4 底层核心:Spring 三级缓存如何解决循环依赖?
上述解决方案是 "应用层" 的规避手段,而 Spring 容器本身具备解决单例 Bean 循环依赖的能力,核心依赖 "三级缓存" 机制。
1.4.1 三级缓存的定义(Spring 源码核心容器)
Spring 通过三个 Map(缓存)存储不同状态的 Bean,定义在DefaultSingletonBeanRegistry类中:
- 一级缓存(singletonObjects):存储 "完全初始化完成" 的单例 Bean,key 是 Bean 名称,value 是完整 Bean 实例(最终供外部使用);
- 二级缓存(earlySingletonObjects):存储 "提前暴露的未完全初始化 Bean",key 是 Bean 名称,value 是已实例化但未完成属性填充和初始化的 Bean 实例;
- 三级缓存(singletonFactories) :存储 "Bean 工厂",key 是 Bean 名称,value 是
ObjectFactory接口实现(用于生成未完全初始化的 Bean 实例,支持 AOP 代理对象的创建)。
1.4.2 三级缓存解决循环依赖的完整流程(以 A 依赖 B、B 依赖 A 为例)
-
创建 Bean A:
- Spring 容器启动,首先尝试获取 Bean A,发现一级缓存、二级缓存均无;
- 执行 Bean A 的 "实例化"(通过构造器创建 A 的空实例,属性未赋值);
- 将 Bean A 的 "工厂对象"(
ObjectFactory)存入三级缓存,此时 Bean A 处于 "未完全初始化" 状态; - 进入 Bean A 的 "属性填充" 阶段,发现需要注入 Bean B。
-
创建 Bean B:
- 尝试获取 Bean B,一、二级缓存均无,执行 Bean B 的 "实例化";
- 将 Bean B 的工厂对象存入三级缓存;
- 进入 Bean B 的 "属性填充" 阶段,发现需要注入 Bean A。
-
解决循环依赖 :

- 尝试获取 Bean A,一级缓存无,但三级缓存中有 Bean A 的工厂对象;
- 调用工厂对象生成 Bean A 的未完全初始化实例(若 Bean A 需要 AOP 代理,此时会创建代理对象);
- 将 Bean A 的实例从三级缓存移至二级缓存(避免重复创建);
- 将 Bean A 的实例注入到 Bean B 中,完成 Bean B 的 "属性填充";
- 执行 Bean B 的 "初始化" 流程,完成后将 Bean B 存入一级缓存;
- Bean B 创建完成,回到 Bean A 的 "属性填充" 阶段,将 Bean B 注入到 Bean A 中;
- 执行 Bean A 的 "初始化" 流程,完成后将 Bean A 从二级缓存移至一级缓存。
-
最终状态:一级缓存中存在完整的 Bean A 和 Bean B,二级缓存和三级缓存中不再存储这两个 Bean。
1.4.3 为什么需要三级缓存?二级缓存不够吗?
核心原因是支持 AOP 代理:
- 若仅用二级缓存,Bean 实例化后直接暴露到二级缓存,但如果该 Bean 需要被 AOP 代理,注入的会是 "原始 Bean 实例" 而非 "代理对象",导致后续使用时出现问题;
- 三级缓存存储的是 "工厂对象",在需要注入时才通过工厂生成实例(若有 AOP 则生成代理对象),确保注入的是代理后的 Bean,同时避免提前创建代理对象造成资源浪费。
1.4.4 三级缓存的局限性
- 仅支持单例 Bean的循环依赖(原型 Bean 每次创建新实例,无法缓存);
- 仅支持非构造器注入(构造器注入会在实例化阶段就依赖 Bean,此时缓存中尚无可用实例)。
二、开发规范:req、res、实体类包的设计逻辑与实践
在实际开发中,合理划分包结构是工程化的基础,req(请求包)、res(响应包)和entity(实体类包)是分层架构中最核心的三个包,其设计直接影响代码的可维护性、安全性和扩展性。
2.1 三个包的核心职责与设计目标
2.1.1 req 包:请求参数的 "接收与校验"
- 核心职责:专门存储 "接收前端请求参数" 的 DTO(数据传输对象),负责参数的接收、格式转换和合法性校验;
- 设计目标 :
- 屏蔽内部业务逻辑,只暴露前端需要传递的字段;
- 通过注解(如 JSR-380 规范的
@NotNull、@Size)实现参数校验,减少业务层校验代码; - 适配前端请求格式(如 JSON、表单),与 Controller 层紧密配合。
典型示例:
java
// UserController的请求DTO
package com.example.demo.req;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data // Lombok注解,自动生成getter/setter/toString等方法
public class UserAddReq {
// 用户名:不能为空,长度2-20
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
// 邮箱:不能为空,格式正确
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
// 年龄:1-120岁
@Size(min = 1, max = 120, message = "年龄必须在1-120之间")
private Integer age;
}
Controller 层使用:
java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// @Validated触发参数校验
@PostMapping("/add")
public UserAddRes addUser(@Validated @RequestBody UserAddReq req) {
// 校验通过后,调用服务层处理
Long userId = userService.addUser(req);
return new UserAddRes(200, "创建成功", userId);
}
}
2.1.2 res 包:响应结果的 "统一与封装"
- 核心职责:专门存储 "返回给前端的响应数据" DTO,负责统一响应格式、屏蔽敏感字段、传递业务状态;
- 设计目标 :
- 统一接口响应格式(如 code+message+data),让前端统一解析;
- 隐藏内部实体的敏感字段(如用户密码、数据库主键规则);
- 区分不同业务场景的响应(如列表响应、详情响应),按需返回字段。
典型示例:
java
// 统一响应基类
package com.example.demo.res;
import lombok.Data;
@Data
public class BaseRes<T> {
// 响应状态码(200成功,500失败,自定义业务码)
private Integer code;
// 响应信息
private String message;
// 响应数据(泛型适配不同类型)
private T data;
// 成功响应静态方法(简化调用)
public static <T> BaseRes<T> success(T data) {
return new BaseRes<>(200, "操作成功", data);
}
// 失败响应静态方法
public static <T> BaseRes<T> fail(Integer code, String message) {
return new BaseRes<>(code, message, null);
}
}
// 用户创建响应(具体业务响应)
package com.example.demo.res;
import lombok.Data;
import lombok.EqualsAndHashCode;
// 继承统一基类
@Data
@EqualsAndHashCode(callSuper = true)
public class UserAddRes extends BaseRes<Long> {
// 可扩展业务特定字段(如创建时间)
private Long createTime;
public UserAddRes(Integer code, String message, Long data) {
super(code, message, data);
this.createTime = System.currentTimeMillis();
}
}
// 用户列表响应(按需返回字段)
package com.example.demo.res;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class UserListRes extends BaseRes<List<UserListItem>> {
// 列表响应可增加分页信息
private Long total; // 总条数
private Integer pageSize; // 每页条数
private Integer pageNum; // 当前页码
// 构造方法略
}
// 列表项DTO(仅返回前端需要的字段)
@Data
public class UserListItem {
private Long id;
private String username;
private String email;
// 不返回密码等敏感字段
}
2.1.3 entity 包:业务实体的 "核心映射"
- 核心职责 :存储与业务核心概念对应的实体类,主要分为两类:
- 数据库实体(ORM 实体):与数据库表结构一一对应(如 JPA 的
@Entity、MyBatis 的@TableName); - 业务实体(领域模型):封装核心业务逻辑和属性,不依赖数据库结构;
- 数据库实体(ORM 实体):与数据库表结构一一对应(如 JPA 的
- 设计目标 :
- 数据库实体:准确映射表结构,包含主键、字段、关联关系(一对一、一对多等);
- 业务实体:封装业务规则(如用户状态流转、订单计算逻辑),是业务逻辑的核心载体。
典型示例:
java
// 数据库实体(MyBatis示例)
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user") // 对应数据库表t_user
public class UserEntity {
@TableId(type = IdType.AUTO) // 自增主键
private Long id; // 对应表中id字段
private String username; // 对应表中username字段
private String email; // 对应表中email字段
private String password; // 数据库存储密码(加密后),前端不可见
private Integer age; // 对应表中age字段
private Integer status; // 状态:0-禁用,1-正常
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
// 业务实体(领域模型示例)
package com.example.demo.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class OrderEntity { // 订单领域模型
private Long orderId;
private Long userId;
private BigDecimal amount; // 订单金额
private Integer status; // 订单状态:0-待支付,1-已支付,2-已取消
// 业务逻辑封装:订单支付
public void pay() {
if (this.status != 0) {
throw new RuntimeException("只有待支付订单可支付");
}
this.status = 1;
// 其他支付相关逻辑(如扣减库存、生成支付记录)
}
// 业务逻辑封装:订单取消
public void cancel() {
if (this.status != 0) {
throw new RuntimeException("只有待支付订单可取消");
}
this.status = 2;
// 其他取消相关逻辑(如恢复库存)
}
}
2.2 三个包的核心区别与设计原则(表格对比)
| 对比维度 | req 包(请求 DTO) | res 包(响应 DTO) | entity 包(实体类) |
|---|---|---|---|
| 核心用途 | 接收前端请求参数 | 返回前端响应数据 | 映射数据库 / 封装业务逻辑 |
| 依赖对象 | 依赖前端请求格式(JSON / 表单) | 依赖前端展示需求 | 依赖数据库表结构 / 业务规则 |
| 字段特点 | 仅包含前端需要传递的字段 | 仅包含前端需要展示的字段(屏蔽敏感信息) | 包含完整业务字段(含敏感字段、关联字段) |
| 核心注解 | 校验注解(@NotBlank、@Email、@Size) | 序列化注解(@JsonIgnore、@JsonProperty) | ORM 注解(@TableName、@Entity、@TableId) |
| 生命周期 | 随请求创建,请求结束销毁 | 随响应创建,响应结束销毁 | 数据库实体:与事务绑定;业务实体:随业务流程创建销毁 |
| 可变性 | 高(前端需求变化时需修改) | 中(前端展示变化时需修改) | 低(数据库结构 / 业务规则相对稳定) |
| 常见误区 | 包含业务逻辑、字段过多 | 返回敏感字段(如密码)、格式不统一 | 直接暴露给前端、与 req/res 混用 |
2.3 为什么不能混用?(工程化实践的核心意义)
很多新手开发会图方便,直接将entity类作为req或res对象传递,这种做法看似省事,实则存在严重问题:
2.3.1 安全风险:敏感字段泄露
entity类中包含数据库主键、密码、状态码等敏感字段,若直接返回给前端,可能导致数据泄露。例如:
java
// 错误示例:直接返回UserEntity
@GetMapping("/detail")
public UserEntity getUserDetail(Long id) {
return userService.getById(id); // 返回包含password的实体
}
前端会直接获取到password字段(即使加密,也不应暴露),违反数据安全规范。
2.3.2 耦合严重:牵一发而动全身
若entity类与数据库表结构绑定,当数据库表新增 / 删除字段时,前端请求和响应也会被迫修改,导致 "数据库变更→req/res 变更→前端变更" 的连锁反应,不符合 "高内聚低耦合" 的设计原则。
2.3.3 冗余字段:传输效率低
entity类包含大量前端不需要的字段(如createTime、updateTime、status等),直接传递会增加网络传输量,降低接口响应速度。
2.3.4 校验混乱:业务逻辑冗余
entity类的字段校验若同时适配数据库和前端,会导致校验规则混乱(如数据库字段允许为空,但前端请求必须传递该字段),增加业务层的校验负担。
2.4 最佳实践:req→entity→res 的流转流程
在分层架构中,三个包的标准流转流程如下:
- 前端发送请求,Controller 层通过
req类接收参数并校验; - Controller 层将
req对象转换为entity对象(可通过 MapStruct、BeanUtils 工具类); - Service 层接收
entity对象,执行业务逻辑(如数据库操作、业务规则校验); - Service 层返回处理后的
entity对象; - Controller 层将
entity对象转换为res对象(屏蔽敏感字段),返回给前端。
(注意:开发中可能会有在Service层中就直接使用Res)
示例代码(MapStruct 转换):
java
// 1. 定义转换器接口
@Mapper(componentModel = "spring")
public interface UserConverter {
// req转entity
UserEntity reqToEntity(UserAddReq req);
// entity转列表项res
UserListItem entityToListRes(UserEntity entity);
}
// 2. Service层使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserConverter userConverter;
public Long addUser(UserAddReq req) {
// req转entity
UserEntity entity = userConverter.reqToEntity(req);
// 补充业务字段(如密码加密、状态默认值)
entity.setPassword(encryptPassword("123456")); // 初始密码加密
entity.setStatus(1); // 默认正常状态
entity.setCreateTime(LocalDateTime.now());
userMapper.insert(entity);
return entity.getId();
}
public List<UserListItem> getUserList() {
List<UserEntity> entities = userMapper.selectList(null);
// entity转res
return entities.stream()
.map(userConverter::entityToListRes)
.collect(Collectors.toList());
}
// 密码加密方法(示例)
private String encryptPassword(String password) {
return DigestUtils.md5DigestAsHex(password.getBytes());
}
}
三、逻辑思维题:分步推导 + 验证,破解采购录音笔问题
3.1 题目复述与核心条件梳理
题目:李亮和刘纯两人采购同一款录音笔,价格规则如下:
- 一次购买数量不足 50 个:单价 176 元;
- 一次购买 50~99 个:单价 160 元;
- 一次购买 100 个及以上:单价 144 元。
已知条件:
- 两人分别购买,总费用 18688 元;
- 两人合买,可节省 2416 元;
- 求:两人中购买数量较少者买了多少个?
3.2 核心解题思路:从 "合买节省" 反推总数量
逻辑题的关键是 "找到突破口",本题的突破口是 "合买节省的费用"------ 合买节省的本质是 "单价降低导致总费用减少",因此第一步应先计算合买的总费用和总数量。
步骤 1:计算合买总费用
分别购买总费用 = 18688 元,合买节省 2416 元,因此:合买总费用 = 18688 - 2416 = 16272 元
步骤 2:计算合买总数量
根据价格规则,合买总数量 N 对应的单价只有三种可能(176、160、144 元),且总数量 N 是正整数,因此通过 "总费用 ÷ 单价" 是否为整数来判断:
- 若 N<50:单价 176 元,N=16272÷176≈92.45(非整数,排除);
- 若 50≤N≤99:单价 160 元,N=16272÷160=101.7(非整数,排除);
- 若 N≥100:单价 144 元,N=16272÷144=113(整数,符合条件)。
结论:两人合买总数量为 113 个,即李亮购买数量 x + 刘纯购买数量 y = 113(设 x≤y,求 x)。
步骤 3:梳理分别购买时的价格档位组合
两人分别购买时,x 和 y 的总和是 113,因此 x 和 y 的档位组合只能有以下 3 种可能(排除矛盾组合):
| 组合类型 | x 的档位(数量范围) | y 的档位(数量范围) | 核心依据 |
|---|---|---|---|
| 组合 1 | x<50 | y<50 | x+y<100,但实际 x+y=113,矛盾,排除 |
| 组合 2 | x<50 | 50≤y≤99 | x+y=113,y=113-x≥113-49=64(64 在 50~99 之间,符合条件) |
| 组合 3 | x<50 | y≥100 | y=113-x≥100 → x≤13,此时 x<50、y≥100,需验证总费用是否符合 |
| 组合 4 | 50≤x≤99 | 50≤y≤99 | x+y≥100,且 x+y=113≤99+99=198,需验证总费用是否符合 |
| 组合 5 | 50≤x≤99 | y≥100 | x≥50,y≥100 → x+y≥150,但实际 x+y=113,矛盾,排除 |
| 组合 6 | x≥100 | y≥100 | x+y≥200,矛盾,排除 |
因此,仅需验证组合 2、组合 3、组合 4 三种可能。
步骤 4:逐一验证组合,计算 x 的值
设分别购买总费用为 18688 元,根据不同组合的单价的计算:
组合 2:x<50(单价 176 元),50≤y≤99(单价 160 元)
核心公式:176x + 160y = 18688,且 x + y = 113 → y=113 - x
代入公式:176x + 160 (113 - x) = 18688展开计算:176x + 160×113 - 160x = 1868816x + 18080 = 1868816x = 18688 - 18080 = 608x = 608 ÷ 16 = 38
验证:x=38<50,y=113-38=75(50≤75≤99),符合档位要求。
组合 3:x<50(单价 176 元),y≥100(单价 144 元)
核心公式:176x + 144y = 18688,且 y=113 - x
代入公式:176x + 144 (113 - x) = 18688展开计算:176x + 144×113 - 144x = 1868832x + 16272 = 1868832x = 18688 - 16272 = 2416x = 2416 ÷ 32 = 75.5(非整数,排除)
组合 4:50≤x≤99(单价 160 元),50≤y≤99(单价 160 元)
核心公式:160x + 160y = 18688 → 160 (x+y)=18688
代入 x+y=113:160×113 = 18080 ≠ 18688(不符合总费用,排除)
步骤 5:验证答案的唯一性
通过上述验证,只有组合 2 符合所有条件,即 x=38,y=75。因此,两人中购买数量较少者买了 38 个。
3.3 易错点提醒:避免遗漏档位组合
本题的常见错误是 "忽略部分档位组合",例如:
- 直接假设两人分别购买时都是 "不足 50 个",但 x+y=113,不可能两人都不足 50 个;
- 忘记 "合买时单价降低的核心逻辑",直接从分别购买的总费用反推,导致计算复杂。
核心原则:逻辑题需 "穷尽所有可能,逐一验证排除",尤其是涉及 "区间档位" 的题目,必须确保每个组合都被覆盖。
四、面试复盘与核心收获
这场面试的三个问题,看似覆盖不同领域,实则考察了技术开发的三大核心能力:
- 底层原理理解能力:Spring Boot 依赖循环不仅要知道 "怎么解决",还要明白 "为什么能解决"(三级缓存),这要求我们不能只停留在 "API 调用" 层面,要深入源码理解设计逻辑;
- 工程化实践能力:req、res、entity 包的设计,体现的是 "分层思想" 和 "规范意识"------ 优秀的代码不仅要能运行,还要可维护、可扩展、安全,这是从 "新手" 到 "资深开发" 的关键;
- 逻辑思维能力:采购问题考察的是 "拆解问题、逐步推导" 的能力,开发中遇到的复杂业务逻辑(如订单计算、权限控制),本质上都是 "逻辑题",需要清晰的思路和严谨的验证。
后续学习建议
- 针对 Spring Boot 底层:深入学习 Bean 生命周期、三级缓存源码、AOP 实现机制,推荐阅读《Spring 实战》和 Spring 官方文档;
- 针对工程规范:学习 RESTful API 设计、DTO 转换工具(MapStruct)、参数校验框架(Hibernate Validator),参与开源项目熟悉规范;
- 针对逻辑思维:定期做算法题和逻辑题,培养 "拆解问题、验证假设" 的习惯,将逻辑思维融入日常业务开发。
END
如果觉得这份基础知识点总结清晰,别忘了动动小手点个赞👍,再关注一下呀~ 后续还会分享更多有关面试问题的干货技巧,同时一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟