电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析
"您好,我是面试官王工,从今天这个开场白开始,希望你接下来每一句话都经得起推敲。"面试官推了推眼镜,对面坐着一位精神抖擞、眼神中透着一丝"背了很多题"的求职者,谢飞机。
谢飞机:"面试官好,我叫谢飞机,大家都叫我飞机哥。我深耕Java八年,从Java 8到Java 17,从Spring Boot到Spring Cloud,从MySQL到Redis,从Kafka到Kubernetes,八百里外我能取一个微服务首级,三秒钟内我能定位一个JVM OOM。总之,技术这块,拿捏得死死的。"
面试官:"很好,那我们直接开始。"
第一轮:核心语言与基础框架
面试官:"先聊点基础。Java 8 和 Java 17 有什么区别?"
谢飞机:"这个简单。Java 8 是2014年出的,有 Lambda 表达式和 Stream 流,Optional 也好用。Java 17 是 LTS 版本,里面有 text block、sealed class、instanceof 模式匹配,还有 switch 表达式。反正 Java 8 能干的活,17 都能干,17 还更香。"
面试官:"那 JVM 内存结构说说看?"
谢飞机:"堆、栈、方法区嘛。堆里面放对象,栈里面放局部变量,方法区放类信息。还有程序计数器,虚拟机栈,本地方法栈。垃圾回收主要在堆里搞。"
面试官:"不错。那 Spring Boot 自动配置的原理是什么?"
谢飞机:"自动配置就是......把以前要写的配置都自动配好了。它有一个 @EnableAutoConfiguration 注解,会去读 META-INF 下面的 spring.factories 或者 AutoConfiguration.imports,然后条件注解判断,比如 @ConditionalOnClass,@ConditionalOnMissingBean。但是到底怎么加载的,嗯,我没仔细看,反正它自己就配置好了。"
面试官:"你如果遇到自动配置不生效,怎么办?"
谢飞机:"先看依赖有没有加,再看是不是有自定义 Bean 冲突了,再不行......我就重启一下项目。"
面试官:"......好吧。数据库连接池 HikariCP 为什么快?"
谢飞机:"它号称最快的连接池,因为用了字节码优化,还用了 ConcurrentBag 这种无锁集合。另外它默认最小化连接数,连接生命周期管理得好。反正我配置里直接写上 HikariCP 就行了。"
面试官:"那 MyBatis 和 JPA(Hibernate)在实际项目里怎么选?"
谢飞机:"我一般是看哪个熟用哪个。MyBatis 写 SQL 灵活,适合复杂查询;JPA 开发快,适合简单 CRUD。如果遇到复杂查询,MyBatis 直接写 SQL,JPA 可以用 @Query 写 JPQL,再复杂一点就写原生 SQL,实在不行就换 MyBatis 呗。"
面试官:"要是复杂查询还要动态排序、多表关联、分页,你怎么取舍?"
谢飞机:"呃......那就 MyBatis-Plus 吧,有 LambdaQueryWrapper,还能分页插件,挺方便的。"
面试官:"你连 MyBatis-Plus 都搬出来了。行,我们进入下一轮。"
第二轮:微服务、缓存与消息队列
面试官:"假设我们现在做一个电商系统。用户下单后,订单服务需要调用库存服务扣减库存。服务注册发现用的什么?"
谢飞机:"可以用 Eureka,也可以 Nacos,Consul 也行。我们项目用的 Spring Cloud,服务之间用 OpenFeign 调用,加一个 @FeignClient 接口,写个方法就能调了。"
面试官:"那如果库存服务响应很慢,甚至挂了,你怎么处理?"
谢飞机:"给 OpenFeign 配超时时间,服务端配 Resilience4j 做熔断降级,比如超时重试或者直接走 fallback 方法。"
面试官:"重试会不会导致重复扣库存?"
谢飞机:"那就要做幂等性了。可以用 Redis 存一个全局唯一 ID,处理过的请求直接拒绝。不过具体我没自己实现过,大概思路是这样。"
面试官:"商品详情页的 Redis 缓存,如何避免缓存穿透?"
谢飞机:"这个我会!查询一个不存在的 key,每次都会打到数据库。解决方法是把查询的空结果也缓存起来,设置一个短过期时间;或者用布隆过滤器,先判断 key 是否存在,不存在就直接返回。"
面试官:"那缓存击穿和雪崩呢?"
谢飞机:"击穿就是一个热点 key 失效,一瞬间大量请求直接打到数据库,可以用互斥锁或者逻辑过期。雪崩就是很多 key 同时过期,可以在设置过期时间时加一个随机数,避免同一时间集体失效。"
面试官:"回答得还挺顺。那下单后,你给 Kafka 发消息,怎么保证消息不丢?"
谢飞机:"生产者设置 acks=all,副本同步确认。消费者的话,关闭自动提交,手动提交 offset。这样消息就不会丢了吧?"
面试官:"如果消费成功了,但是业务处理完提交 offset 失败了,会发生什么?"
谢飞机:"那就重复消费了呗。所以消费逻辑要做幂等,比如用数据库唯一约束或者 Redis setnx 做去重。"
面试官:"那订单创建和扣库存这两个操作,怎么保证最终一致性?"
谢飞机:"这个......可以用本地消息表,或者用 Seata 做一个分布式事务。但是 Seata 那个 AT 模式我没有深入了解,反正就是写一个 @GlobalTransactional 注解就完事了。"
面试官:"@GlobalTransactional 背后的原理是什么?"
谢飞机:"它就是......全局事务协调器嘛,回滚日志什么的。但我没看过源码,担心说了不准。"
面试官:"行,那我们再聊聊安全。"
第三轮:安全、测试与部署
面试官:"电商登录,怎么用 Spring Security 和 JWT 做无状态认证?"
谢飞机:"用户登录成功后,后端生成一个 JWT 返回给前端,前端每次请求放到 Authorization 头里。然后我们在 Spring Security 里写一个过滤器,解析 JWT,验证签名,把用户信息设置到 SecurityContext 里。"
面试官:"JWT 密钥怎么管理?如果泄露了怎么办?"
谢飞机:"密钥放配置文件里。泄露了就......换一个密钥,所有用户重新登录呗。"
面试官:"那如果用户权限变了,如何让 JWT 立即失效?"
谢飞机:"啊?JWT 不是无状态的吗?要立即失效的话,可以用 Redis 存黑名单。但是我没实际做过,我一般只做登录和解析。"
面试官:"再问个场景:用户快速点击支付按钮,如何保证接口幂等性?"
谢飞机:"这个我会!前端生成一个全局唯一请求 ID,后端接收后先用 Redis 的 setnx 判断这个 ID 有没有处理过,如果处理过就返回重复请求;第一次请求就把 ID 存起来,设置过期时间。整个 setnx 和过期时间要用 Lua 脚本保证原子性。"
面试官:"你终于说对一个完整的方案了。那 Mockito 单元测试怎么用?"
谢飞机:"用 @Mock 创建一个接口的 mock 对象,然后 when(...).thenReturn(...),验证用 verify,比如 mockUserMapper.selectById(1),when,thenReturn(user),然后调用 service 后 verify 一下。"
面试官:"PowerMock 能 mock 静态方法,你了解吗?"
谢飞机:"了解一点,比如 mockStatic 的语法,但是我们项目主要用 Mockito,静态方法一般用工具类包一层,不用 PowerMock。"
面试官:"那 Docker 和 Kubernetes 部署呢?Dockerfile 怎么写?"
谢飞机:"简单:FROM openjdk:17,COPY app.jar /app.jar,ENTRYPOINT java -jar /app.jar。然后 build 之后 docker run 就行。"
面试官:"Kubernetes 里怎么做滚动更新?"
谢飞机:"用 Deployment 啊,改一下镜像版本,它会自动滚动升级。但是具体的 YAML 配置我记得不太全,什么 strategy 的 RollingUpdate 设置 maxSurge、maxUnavailable 那些,我都是复制粘贴的。"
面试官:"如果 Pod 崩溃了,怎么排查?"
谢飞机:"先 kubectl logs 看日志,然后 kubectl describe pod 看事件,如果还不行就 kubectl exec 进去看看,最后不行就重启。"
面试官:"好了,今天的面试到这里。感谢你参加我们的面试,我们会在三个工作日内通知你面试结果。你先回去等通知吧。"
谢飞机:"好的,面试官,我回去等好消息!对了,我可以发一下我的开源项目吗?虽然只有一个 star,但是是我自己写的。"
面试官:"嗯,简历上有我们就看。下一位。"
面试问题详细答案
下面把面试中涉及的关键问题逐一展开,结合电商业务场景讲清楚技术点和学习方向,方便小白们系统理解。
1. Java 8 和 Java 17 的主要区别有哪些?
核心点:Java 8 和 Java 17 都是 LTS(长期支持)版本,但中间隔了 9 个大版本。Java 8 引入了函数式编程(Lambda、Stream、Optional)、新的日期时间 API、默认方法;Java 17 则在语言和 API 上做了大量增强。
Java 8 重点:
- Lambda 表达式:简化匿名内部类,比如
list.forEach(x -> System.out.println(x))。 - Stream API:集合的链式流式操作,支持 map/filter/reduce。
- Optional:避免空指针的容器对象。
- 新日期时间 API:
LocalDate、LocalDateTime、Instant等,线程安全。 - 接口默认方法和静态方法。
Java 17 新增重点:
sealed class(密封类):限制哪些类可以继承。text block(文本块):用三引号写多行字符串,不再需要\n。instanceof模式匹配:if (obj instanceof String s) { ... }。switch表达式:可以返回值,且支持箭头语法。- 新增
Record(Java 16 正式):定义不可变数据类,替代 Lombok 的部分功能。 - 移除了一些过时的工具,比如
AOT相关特性,也增强了ZGC、Shenandoah等垃圾回收器。
业务场景:新项目建议直接上 Java 17 或 21;老项目如果要升级,需要主要关注依赖兼容性,比如 Lombok、CGLIB 等。面试时能说清两代 LTS 的区别,已经可以超过大部分候选人。
2. JVM 内存结构是什么样的?
JVM 内存区域根据 Java 虚拟机规范分为以下几个部分:
- 程序计数器:当前线程执行的字节码行号指示器,线程私有,无 OOM。
- 虚拟机栈 :线程私有,每个方法调用对应一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口。深度超限会抛
StackOverflowError。 - 本地方法栈:为 JVM 使用的底层本地方法服务,线程私有。
- 堆:线程共享,存放对象实例和数组,是 GC 回收的主要区域。通常分为新生代(Eden、Survivor)和老年代。
- 方法区:线程共享,存储类元信息、常量、静态变量等。JDK 8 之后用元空间(Metaspace)实现,不再使用永久代,元空间使用本地内存。
- 运行时常量池:方法区的一部分,存放编译期生成的字面量和符号引用。
学习建议:要能画图,能说明每个区域是否线程私有、会不会 OOM,以及垃圾回收发生的位置。面试官常追问"对象创建过程"和"full GC 触发条件",可以继续深入。
3. Spring Boot 自动配置的原理是什么?
Spring Boot 的自动配置核心是 @EnableAutoConfiguration。它通过 @Import 引入 AutoConfigurationImportSelector,该组件会扫描所有依赖的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 2.7 之前是 spring.factories),把所有候选的自动配置类加载进来。
但加载进来不代表一定生效,自动配置类上通常有一堆条件注解:
@ConditionalOnClass:类路径下存在某个类才生效。@ConditionalOnMissingBean:容器中没有某个 Bean 才生效,给用户覆盖的机会。@ConditionalOnProperty:配置项满足条件才生效。
比如 RedisAutoConfiguration:
java
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { ... }
}
业务场景 :当你想自定义 RedisTemplate,只要在容器中声明一个叫 redisTemplate 的 Bean,自动配置就会退让。
常见追问:
spring.factories和AutoConfiguration.imports区别?后者是 2.7 之后的新加载方式,支持排序。- 自动配置和普通
@Configuration的区别?自动配置类也是普通配置,但优先级低,通常放在自动配置包中。
4. HikariCP 为什么快?和 C3P0 相比有什么优势?
HikariCP 是目前 Spring Boot 2.x/3.x 默认的连接池。它快在几个地方:
- 字节码精简:HikariCP 的字节码经过优化,方法体更小,CPU 缓存命中率更高。
- 无锁数据结构 :使用
ConcurrentBag,在获取和归还连接时采用 ThreadLocal 存储和 steal 机制,减少锁竞争。 - 代理机制优化 :连接代理类使用
javassist生成,避免通过 JDK 动态代理的大量反射开销。 - 连接生命周期管理 :默认
maximumPoolSize=10,minimumIdle与连接创建策略十分高效;还支持maxLifetime防止服务器回收连接。
C3P0 是老牌连接池,但较重、性能一般,且配置复杂。在实际电商高并发场景,数据库连接是稀缺资源,连接池等待时间直接影响接口 RT,所以选 HikariCP。
配置示例:
yaml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
学习建议 :理解连接池概念(初始化、活跃、空闲、等待队列),并会分析连接池耗尽导致的 Connection is not available 异常。
5. MyBatis 和 JPA(Hibernate)如何选择?复杂查询怎么办?
这是一个经典问题。
JPA(Hibernate):
- 优点:对象关系映射完整,CRUD 开发效率极高,缓存机制成熟,多表关联可以用实体关系自动管理。
- 缺点:复杂查询 N+1 问题容易踩坑,SQL 优化不直观,动态 SQL 能力弱。
MyBatis:
- 优点:SQL 完全由开发者控制,方便 DBA 审查和优化,动态 SQL 非常灵活(
<if>、<foreach>等),适配复杂查询。 - 缺点:需要手写大量 SQL,关联映射需要配置,维护成本高。
实际业务选型建议:
- 单表 CRUD、后台管理系统、实体关系简单:用 JPA 或 Spring Data JPA。
- 电商交易、C 端高并发查询、复杂多表统计、需要精细控制 SQL:用 MyBatis/MyBatis-Plus。
- 也可以混合使用:倾向 Spring Data JDBC + MyBatis。
复杂查询方案:
- MyBatis 直接写 XML 或注解 SQL,用动态 SQL 处理条件。
- JPA 用
@Query(JPQL)或nativeQuery = true。 - 更复杂的聚合查询,可以把数据放到 Elasticsearch 或 ClickHouse 中。
- 不要把所有东西都塞进口 ORM,分库分表后还要考虑路由。
6. 微服务架构中服务发现和调用是怎么工作的?
电商微服务常见架构:
- 服务提供者启动时,把自己的 IP、端口、服务名注册到注册中心(Eureka / Nacos / Consul)。
- 服务消费者从注册中心拉取可用服务列表,并缓存到本地。
- 服务消费者通过 OpenFeign 声明式 HTTP 客户端发起调用。OpenFeign 底层是 Ribbon 或 Spring Cloud LoadBalancer,从本地列表中负载均衡选择一个实例。
Eureka 工作过程:
Eureka Client注册,发送心跳,默认 30 秒一次。- 90 秒未收到心跳,Eureka Server 会剔除实例。
- 消费者本地缓存服务列表,默认每 30 秒拉取一次。
Consul 不同:使用 Consul 作为注册中心,它基于共识协议(Raft),支持多数据中心,还提供键值存储、健康检查。服务注销比 Eureka 更及时。
OpenFeign 核心:
java
@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
@PostMapping("/inventory/deduct")
Response deduct(@RequestBody DeductRequest request);
}
调用时通过动态代理生成 HTTP 请求,集成 Ribbon/LoadBalancer,还会走 Resilience4j 或 Sentinel 容错。
注意:如果注册中心挂了,本地缓存列表还能继续用一段时间,前提是消费者实例不重启。生产环境建议部署多实例注册中心。
7. 服务超时和故障如何容错?Resilience4j 怎么用?
库存服务依赖数据库、网络,可能变慢或宕机。如果订单服务对库存服务的调用一直阻塞,线程池会被耗尽,导致整个应用雪崩。
Resilience4j 是一个轻量级容错库,提供:
- CircuitBreaker(熔断器):当错误率达到阈值,熔断打开,后续请求直接降级,不再调用下游。
- RateLimiter(限流):限制请求速率。
- Bulkhead(隔离):限制并发调用数,防止下游故障耗尽线程。
- Retry(重试):对临时性失败自动重试。
- TimeLimiter(超时):限制调用时长。
示例:
yaml
resilience4j:
circuitbreaker:
instances:
inventoryService:
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
slidingWindowSize: 10
retry:
instances:
inventoryService:
maxAttempts: 3
waitDuration: 200ms
重点提示 :重试要谨慎,要配合幂等。扣库存接口必须支持幂等,否则重试会重复扣减。可以设计一个 deduct_token 作为请求唯一键,库存表加唯一约束或 Redis 去重。
8. Redis 如何解决缓存穿透、击穿、雪崩?
电商商品详情页是最常见的缓存场景。数据流程:先查 Redis,没有则查 MySQL,把结果写回 Redis。
缓存穿透:查询一个并发很高的不存在的 key,每次都打到数据库。
- 方案一:缓存空值,设置 3~5 分钟过期。问题:空值过多,且同一时间依然会打到数据库。
- 方案二:布隆过滤器,把所有可能存在的 key 的哈希映射到一个位数组。不存在直接拦截。问题是布隆过滤器有误判,且删除复杂。
- 方案三:增强参数校验,比如非法 ID 直接返回。
缓存击穿:一个热点 key 失效,比如秒杀商品,瞬间大量请求打过来,全部打到数据库。
- 方案一:互斥锁。只让一个线程去数据库加载,其他线程等待后读取缓存。
- 方案二:逻辑过期。缓存中不设置过期时间,而是设置一个过期时间字段,后台异步刷新,但不保证强一致。
缓存雪崩:大量 key 同时失效,或者 Redis 宕机,导致大量请求打到数据库。
- 方案一:过期时间加随机值,比如
base + random(0, 300)秒。 - 方案二:多级缓存,比如本地 Caffeine + Redis,两级保护。
- 方案三:Redis 高可用,主从+哨兵或 Redis Cluster。
兜底:数据库层面也要做限流、降级、熔断。
9. Kafka 如何保证消息不丢失和重复消费?最终一致性如何实现?
在电商下单场景,下单成功后通常会发一条"订单创建成功"的 Kafka 消息,通知库存、积分、通知等系统。
消息不丢失需要从三个端保证:
- 生产者端:
acks=all,等待所有副本确认;同步发送时Future.get()判断是否成功;失败重试。 - Broker 端:
replication.factor>= 3,min.insync.replicas>= 2,分区 leader 切换不会丢消息。 - 消费者端:
enable.auto.commit=false,业务处理成功后再手动提交 offset。
重复消费必然存在,因为"至少一次"语义。例如消费者处理完消息,但是还没来得及提交 offset,应用崩溃,重启后从头消费。
幂等消费方案:
- 在数据库中建消息消费记录表,唯一键为消息 ID。插入前判断,如果已存在则跳过。
- 或者使用 Redis
setnx msgId value,返回 1 才处理。 - 也可以利用业务表唯一约束,比如订单号唯一。
最终一致性:订单服务和库存服务之间,不能强一致。通常用"本地消息表 + Kafka"或基于 Kafka 事务消息。流程:
- 订单服务在本地数据库开启事务,插入订单,并插入一条消息记录表。
- 事务提交后,后台定时任务把消息发送到 Kafka。
- 库存服务消费消息,执行扣库存,执行成功后发送"扣减完成"消息回来。
- 订单服务通过定时任务扫描未完成的消息,不断重试或对账。
这种方式保证了一方成功、另一方可重试,最终达到一致。如果涉及多个系统,也可以用 Seata 的 AT/TCC 模式,但要注意性能代价。
10. 分布式事务如何处理?(Seata 与本地消息表)
电商核心链路:订单 - 库存 - 支付 - 积分。跨库、跨服务,数据库本地事务解决不了。
常见方案:
- XA 两阶段提交:数据库原生支持,但同步阻塞、性能差。
- TCC(Try-Confirm-Cancel):适合强一致要求高的场景,比如余额扣减。需要业务方实现 Try/Confirm/Cancel 三段逻辑。
- AT 模式(Seata):基于全局事务协调器,通过代理数据源生成 undo log。业务代码简单,但有脏写风险。
- 本地消息表 + MQ:最终一致性,允许短暂不一致。
Seata AT 模式核心:
- 全局事务 ID 通过 Dubbo/Feign 传播。
@GlobalTransactional开启全局事务。- 分支事务注册到 TC,TC 协调提交或回滚。
- 一阶段:业务数据操作 + 记录回滚日志。
- 二阶段:全局提交,删除回滚日志;全局回滚,根据回滚日志恢复数据。
注意:AT 模式依赖数据库事务,数据源必须支持;隔离性受限于全局写锁,不适合极致并发。简单业务中,本地消息表更轻量可靠。
11. Spring Security + JWT 如何实现登录认证和授权?
JWT(JSON Web Token) 结构:Header.Payload.Signature。Payload 中常放 userId、username、role、exp 过期时间。
Spring Security 认证流程:
- 用户 POST
/login,Spring Security 的AuthenticationManager调用UserDetailsService加载用户,比对密码。 - 密码校验通过后,生成 JWT,返回前端。
- 前端后续请求在
Authorization: Bearer <jwt>中携带。 - 后端添加一个
OncePerRequestFilter,解析 JWT,验证签名,如果合法则构造Authentication并放入SecurityContextHolder。 - 在
SecurityFilterChain中配置哪些接口需要认证、哪些是放行资源、使用无状态SessionCreationPolicy.STATELESS。
权限控制:
- 使用
@PreAuthorize("hasRole('ADMIN')")开启方法级安全。 - 在 JWT 的 claims 中写入权限集合,解析后封装为
GrantedAuthority。
JWT 密钥管理:
- 密钥不要硬编码,使用环境变量或配置中心。
- 使用 RS256 非对称签名,私钥放在认证服务,公钥放在资源服务。
- 密钥泄露时,需要撤销所有令牌:可以维护 Redis 白名单/黑名单,或者保持短过期时间(如 10 分钟)。
缺点 :无法主动登出(服务端状态丢失)。解决方案是 Redis 存 token 的 jti 和用户 ID,登出时删除。
12. 如何设计接口幂等性?支付场景怎么做?
用户重复点击支付按钮,或支付回调重复通知,都必须保证只扣一次款。
幂等核心:相同请求只产生一次效果。
常见方案:
-
前端控制:按钮置灰,生成请求唯一 ID。
-
后端使用唯一业务号:比如订单号 + 支付通道 + 支付流水号。
-
使用 Redis SETNX :
SET pay_token_{orderId} 1 NX EX 300只有第一次设置成功才执行支付逻辑,第二次发现 key 存在则直接返回"处理中"。
-
使用数据库唯一约束 :在支付记录表对
order_id建唯一索引,重复插入会冲突,捕获异常后返回已处理。 -
状态机:订单状态从
待支付->支付中->已支付,只有状态满足才允许变更。
Lua 原子操作:为了保证"检查 token 是否存在 + 删除 token + 执行业务"的原子性,可以使用 Lua 脚本。比如:
lua
if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then
return 1
else
return 0
end
注意:分布式锁和幂等是不同的概念。锁是保证并发互斥,幂等是保证重复请求结果一致。支付回调还需要校验签名和金额,才能防止伪造通知。
13. Mockito 和 PowerMock 怎么用?
单元测试是保障代码质量的重要手段。
Mockito 基础:
mock(Class)创建 mock 对象。when(mock.method(args)).thenReturn(result)设置预期行为。verify(mock).method(args)验证方法是否被调用。@Mock、@InjectMocks与 JUnit 5 集成:
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
OrderMapper orderMapper;
@InjectMocks
OrderService orderService;
@Test
void testGetOrderById() {
Order order = new Order();
order.setId(1L);
when(orderMapper.selectById(1L)).thenReturn(order);
Order result = orderService.getOrderById(1L);
assertEquals(1L, result.getId());
verify(orderMapper).selectById(1L);
}
}
Mockito 限制:不能 mock 静态方法、构造方法、私有方法。此时可以用:
- PowerMockito:
PowerMockito.mockStatic(IdGenerator.class),when(IdGenerator.nextId()).thenReturn("123")。 - Mockito 3.4+ 内置 mock
static方法(需要 mockito-inline)。
业务场景:单元测试不启动 Spring 容器,用 mock 隔离外部依赖,让测试快速运行。集成测试才会用 Testcontainers 启动 Redis/MySQL。
14. 如何用 Docker 和 Kubernetes 部署 Java 服务?
Docker 部署:
dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
构建镜像:
bash
docker build -t my-app:1.0.0 .
docker run -d -p 8080:8080 --name my-app my-app:1.0.0
多阶段构建可以更小:
dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre
COPY --from=build /build/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Kubernetes 滚动更新 : 编写 Deployment YAML:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: my-registry/order-service:1.0.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
发布新版本:
bash
kubectl set image deployment/order-service order-service=my-registry/order-service:1.1.0
kubectl rollout status deployment/order-service
滚动更新策略保证先启动一个新 Pod,就绪后再杀掉一个旧 Pod,逐步替换。maxUnavailable=0 表示最少有一个 Pod 可用,服务不中断。
排查问题:
kubectl logs pod-name看应用日志。kubectl describe pod pod-name看事件和状态。kubectl exec -it pod-name -- sh进入容器。
监控 :Java 服务通过 Micrometer 暴露 /actuator/prometheus,Prometheus 抓取指标,Grafana 展示;日志通过 Filebeat 采集到 ELK。
15. 如何做日志监控与链路追踪?
电商系统多为微服务,排查一个请求跨多个服务需要链路追踪。
- 日志:使用 SLF4J + Logback/Log4j2,输出 JSON 格式,包含 traceId。
- 指标:Micrometer 集成到 Spring Boot,提供有意义的监控指标:JVM、线程、HTTP 请求、数据库连接池、Redis、Kafka 消费者 lag。
- 监控:Prometheus 定时拉取,Grafana 画面板,告警在 Alertmanager 中设置。
- 链路追踪:使用 Jaeger 或 Zipkin。Spring Cloud Sleuth(现已变成 Micrometer Tracing)自动生成 traceId/spanId,通过 HTTP 头传递。
- ELK:Filebeat 从容器日志文件采集,推到 Logstash 清洗,存到 Elasticsearch,Kibana 做可视化。
在电商场景中,一次下单请求会经过 gateway、order-service、inventory-service、payment-service。用 traceId 把所有日志串起来,查询一个订单问题能快速定位是哪个服务慢。
结语
谢飞机虽然很多问题答得不够深,但他能记住一些关键名词,说明有一定基础。真正的工程师需要把"背得出来"变成"写得出来",在真实故障中还要能"救得回来"。希望这篇面试实录能帮助准备 Java 面试的读者,把技术点串起来,不仅会背答案,还能在项目中思考"为什么这么设计"。最后,祝大家面试都能拿到满意的 offer,不要像谢飞机一样,回家等通知。