第一轮:基础概念
Q1:一次请求经过哪些层?
正确链路
请求 → [JwtInterceptor:检查Token] → [LogAspect:记日志] → Controller → Service ↓ ← JSON Mapper ↑ ↓ [Jackson 序列化] MySQL ↓ ← Service ← 数据 ← [Redis 缓存拦截]
第二次请求同一接口时,@Cacheable 让 Service 直接走 Redis,连 Mapper 和 MySQL 都不经过。
Q2:category_id → categoryId 谁做的?
✅ MyBatis,不是 Spring MVC
配置在 application.properties:
mybatis.configuration.map-underscore-to-camel-case=trueMyBatis 查完数据库后自动把下划线命名转驼峰。Spring MVC 不管这事。
Q3:没有异常处理 vs 有异常处理
| 阶段 | 查 id=999 | 前端看到 |
|---|---|---|
| 没有异常处理 | Mapper 返回 null → Controller 包装 Result.success(null) | {"code":1,"data":null} 用户懵了 |
| 有异常处理 | Service 发现 null → throw new BusinessException("菜品不存在") → GlobalExceptionHandler 接住 | {"code":0,"msg":"菜品不存在"} ✅ |
Q4:#categoryId 里的 # 是什么?
✅ SpEL 表达式(Spring Expression Language)
#categoryId → 取方法参数 categoryId 的实际值。categoryId=1 → key 是 1
"categoryId" → 字面量字符串,所有分类用同一个 key,查什么都返回同一份缓存
常见 SpEL 表达式
| 表达式 | 含义 |
|---|---|
| #参数名 | 取方法参数的值 |
| #result | 取方法返回值 |
| #对象.属性 | 取对象的属性值 |
Q5:拦截器抛异常会被全局异常处理器捕获吗?
❌ 不会
请求 → [拦截器 preHandle] ← 在 DispatcherServlet 之前 ↓ 抛异常 → 直接返回错误给客户端 DispatcherServlet 还没介入 @RestControllerAdvice 管不到这里
@RestControllerAdvice 只拦截 Controller 层抛出的异常。拦截器在 Controller 之前,必须自己 try-catch 处理。
对比
| JwtInterceptor | GlobalExceptionHandler | |
|---|---|---|
| 部署位置 | Controller 之前 | Controller 之后 |
| 拦截对象 | HTTP 请求 | Controller 抛出的异常 |
| 覆盖范围 | 门禁检查 | Controller → Service → Mapper 全线 |
Q6:form-data vs raw JSON
| 格式 | Content-Type | 适用场景 |
|---|---|---|
| form-data | multipart/form-data | 传文件、文件+文字混合 |
| raw JSON | application/json | 传结构化数据 |
| x-www-form-urlencoded | application/x-www-form-urlencoded | 传简单表单 |
JSON 只支持文本,图片是二进制数据。multipart/form-data 是专门为文件上传设计的协议。
Q7:@Component、@Service、@RestController 的区别?
功能上完全一样,把 @Service 换成 @Component 项目照样跑
区别在语义(分层标签):
@Component ← 通用:"Spring 管理的组件" ├── @Service ← 业务逻辑层 ├── @Repository ← 数据访问层(MyBatis 用 @Mapper 替代) └── @Controller ← 控制层(返回视图) └── @RestController ← 返回 JSON
Q8:@Autowired 和 @Cacheable 谁先生效?
✅ @Autowired 先生效(启动时),@Cacheable 后生效(运行时)
启动时:
- Spring 创建 DishServiceImpl → @Autowired 注入 DishMapper
- → @Cacheable 生成代理对象(包装原对象)
运行时(来请求了):
- → 代理检查 Redis 有没有缓存
- → 有 → 直接返回(不执行方法体,不碰 Mapper/MySQL)
- → 没有 → 执行方法体 → dishMapper 查 MySQL → 结果存 Redis
注入是启动时的事,缓存检查是运行时的事,不在一个时间维度。
第二轮:架构与设计
Q9:为什么 Service 要写接口 + 实现类?
关键原因:Spring AOP 代理机制
Spring 需要生成代理对象来注入 @Cacheable、@Transactional 等魔力:
DishServiceImpl(原始对象) ↓ JDK 动态代理(基于接口) Proxy 对象 = 原始对象 + @Cacheable 拦截 + @CacheEvict 拦截 ↓ Controller 拿到的就是 Proxy,缓存注解才能生效
有接口 → JDK 动态代理(干净)。没接口 → CGLIB 继承代理(也能跑,但有坑:不能代理 final/private 方法)。
Q10:allEntries = true 有什么问题?
问题:删一道菜,全清缓存 → 过度清理
更好的做法是精确清除:
@CacheEvict(cacheNames = "dish", key = "#dish.categoryId") // 只清该分类 @CacheEvict(cacheNames = "dish", key = "'all'") // 只清全部列表Q11:Service 内部 this.xxx() 缓存会生效吗?
❌ 不会
this.getByCategoryId(1L) // ↑ this = 原始对象,没走过 Spring 的 Proxy // @Cacheable 注解 = 摆设外部调用走代理 → 生效。内部 this 走原始对象 → 不生效。
解决方法:注入自己
@Autowired private DishService self; // 注入的是代理对象 self.getByCategoryId(1L); // 走代理 ✅经典 Spring AOP 陷阱,面试高频考点。
第三轮:故障与边界
Q12:Redis 挂了,@Cacheable 方法会怎样?
默认直接抛异常,整个请求失败
但缓存不应该成为单点故障——缓存挂了,业务必须降级跑。
核心思想:缓存是锦上添花,不能因它瘫痪。
拦截器 + 配置可以做到 Redis 不可用时自动跳过缓存、直连数据库。
Q13:数据库插入成功,但清缓存失败 → 不一致
场景
- insert(dish) → 成功 ✅
- 清缓存 → Redis 网络抖动,失败 ❌
- 结果:数据库有新菜,Redis 是旧列表
解决思路
| 方案 | 做法 |
|---|---|
| 先删缓存再更新 | 删缓存 → 更新数据库,查询时重建 |
| 延迟双删 | 删缓存 → 更新 DB → 等一会儿再删一次 |
| TTL 兜底 | 至少过期后会强制刷新,不会永远不一致 |
本项目:allEntries = true + TTL 10 分钟就是最朴素的兜底。
Q14:管理员禁用员工后,旧 JWT 还能用吗?
✅ 能!
JWT 是无状态的"不记名小票"——一旦签发,只要没过期就能用。拦截器不查数据库,不检查 status。
解决方案
| 方案 | 做法 | 代价 |
|---|---|---|
| 每次请求查库 | 拦截器查 status | 每次打 DB |
| Redis 黑名单 | 禁用时 JWT 加入黑名单 | 有状态了 |
| 短 TTL | 15 分钟过期 + Refresh Token | 频繁刷新 |
苍穹外卖:JWT + Redis 黑名单。
全部薄弱点汇总
| 轮次 | 薄弱点 | 正确答案 |
|---|---|---|
| R1 | SpEL # | 取变量值,不是路径参数 |
| R1 | 驼峰转换 | MyBatis 做的 |
| R1 | 拦截器异常 | 不被 @RestControllerAdvice 捕获 |
| R1 | @Component vs @Service | 功能一样,语义不同 |
| R1 | 注入 vs 缓存时序 | 注入启动时,缓存运行时 |
| R2 | 接口 + 实现类 | 为了 JDK 动态代理(AOP) |
| R2 | allEntries = true | 过度清理,应精确清除 |
| R2 | this.xxx() | 绕过代理,缓存不生效 |
| R3 | Redis 挂掉 | 缓存异常应降级,不能拖垮业务 |
| R3 | DB/缓存不一致 | 用 TTL 兜底,至少不会永远不一致 |
| R3 | 禁用后 JWT 仍有效 | 无状态小票的固有缺陷,靠黑名单或短 TTL 解决 |