面灵AI→

满帮集团 27 届全栈开发工程师一面面经

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 数据权限原来为什么要硬编码,后来怎么重构的?
  2. 自定义注解 + AOP + MyBatis 动态 SQL 怎么实现?
  3. Service 内部调用怎么解决 AOP 自调用问题?
  4. 多表、多角色权限怎么处理?
  5. ${} 拼 SQL 有没有注入风险?
  6. 项目中为什么用 CompletableFuture?
  7. 线程池使用要注意什么?线程池隔离、拒绝策略?
  8. ReentrantLock、公平锁 / 非公平锁以及 AQS
  9. MySQL 四种事务隔离级别
  10. 项目性能优化具体怎么做?(多次 SQL 合并成一次条件聚合、把 Java 层聚合下推到数据库)
  11. 微服务里的限流和降级区别
  12. Redis 数据结构;项目有没有用锁?Redis 分布式锁怎么实现?
  13. AI Coding 平时怎么用?
  14. 怎么看全栈?如果入职做全栈有没有挑战?
  15. Vue 生命周期
  16. 行为题:实习中最挫败的事情?Mentor 不认可方案时怎么处理?
  17. 有没有了解 Agent / AI 框架?

《参考解析》

  1. AOP 自调用为什么失效、怎么解决:Spring 的 AOP 靠代理对象生效,this.b() 走的是原始对象而不是代理,所以 b 上的切面(典型是 @Transactional、自定义权限注解)全部不生效。解法有:把 b() 拆到另一个 bean 里通过注入调用;用 AopContext.currentProxy()(需要 @EnableAspectJAutoProxy(exposeProxy = true));注入自身 @Lazy 代理;或者用 BeanFactory 拿到代理对象再调用。代码评审里这类问题很难靠读代码发现,通常要写测试或者加启动期的自检。
  2. ${} 与 #{} 的注入风险:#{} 是预编译占位符,参数走 PreparedStatement,天然防注入;${} 是字符串直接拼接,任何用户可控的值经过它都可能被注入。动态 SQL 里 ${} 唯一合理的用途是拼接不能参数化的部分,比如表名、列名、order by 字段——而且必须用白名单枚举校验,绝不能直接用前端传来的字符串。数据权限场景经常需要动态拼 where,那里尤其要守住这条线。
  3. 多表多角色权限怎么做:把「谁能看哪些数据」抽象成数据范围表达式,由注解声明、切面解析、动态 SQL 生效。落地时通常需要三块:角色到数据范围的映射(全部 / 本部门及下属 / 仅本人 / 自定义)、把当前用户的组织路径查出来(避免每次递归)、以及在 SQL 层把范围条件拼进去。多表的话要明确每个表的权限字段归属,join 之后以主表的范围为准还是取交集,这点没定清楚很容易越权。
  4. CompletableFuture 与线程池隔离:用 CompletableFuture 主要是把多个互相独立的远程调用并行化,把串行的总耗时压到最长的那一个。要注意的是:不传线程池时它默认用 ForkJoinPool.commonPool(),而 commonPool 的并行度是 CPU 核数减一,拿它跑阻塞 IO 会拖垮整个 JVM。所以必须显式传业务线程池,并且不同下游用不同池做隔离——否则一个下游变慢会把池占满,连带其他正常下游一起超时。拒绝策略按业务选:核心链路用 CallerRunsPolicy 做背压,非核心可以直接丢弃并打点。
  5. 限流和降级的区别:限流是入口侧的控制,主动拒绝超出容量的一部分请求,保护系统不被压垮;降级是链路侧的取舍,在依赖不可用时返回兜底结果(缓存、默认值、简化功能),保证主流程还能走通。两者通常配合使用:先限流挡住超额流量,再对非核心依赖做降级。熔断是降级的触发机制之一——失败率超阈值就打开断路器,快速失败一段时间后再半开重试。
  6. Redis 分布式锁的正确写法:加锁用 SET key value NX PX <ttl>,value 必须是每个客户端唯一的标识(UUID 或请求 ID);解锁必须用 Lua 脚本先比对 value 再删,否则可能删掉别人续期后的锁。业务执行时间可能超过 TTL 时要有看门狗续期。更严格的场景要考虑 Redis 主从切换导致锁丢失的问题(主库写入后还没同步就宕机,锁就没了),这时可以用 Redlock 或多副本确认,但也要清楚 Redlock 本身的争议——如果一致性要求极高,用带 fencing token 的方案或直接落数据库。
  7. AQS 与公平 / 非公平锁:AQS 用了一个 volatile 的 state 加一个 CLH 变体的双向等待队列,ReentrantLock 用 state 表示重入次数。非公平锁在 lock() 时先直接 CAS 抢一次,抢不到才入队,所以新来的线程可能插队,吞吐更高但存在饥饿风险;公平锁走 hasQueuedPredecessors() 检查队列里有没有等更久的线程,严格 FIFO,代价是更多的线程切换。默认是非公平。
  8. 性能优化:把 Java 层聚合下推到数据库:原来可能是查回一批明细到 Java 里循环统计,数据量大时既费网络又费内存。下推之后用一次条件聚合(sum(case when ... then 1 else 0 end))或者 group by 直接拿结果,网络传输从 N 行变成几行。前提是聚合能用上索引、不会产生大临时表,改完要对比 EXPLAIN 和实际耗时,别把 Java 的锅换成数据库的锅。