服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 23:30

gRPC 的核心特性与优势是什么?

gRPC 是什么gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 传输协议和 Protocol Buffers 序列化格式构建。它不仅是一个远程调用工具,更是一套完整的跨语言服务通信方案——通过 .proto 文件定义接口,自动生成多语言客户端和服务端桩代码,让不同语言的服务之间高效互通。核心特性HTTP/2 传输层HTTP/2 是 gRPC 高性能的底层基石,带来三个关键能力:多路复用:一条 TCP 连接上并行收发多个请求/响应,彻底解决 HTTP/1.1 的队头阻塞问题。实际效果是微服务间无需维护大量连接池,单连接即可支撑高并发调用。头部压缩(HPACK):对请求头进行差分编码压缩,频繁调用的场景下头部开销可降低 80% 以上,这对移动端和低带宽环境尤其重要。流式传输:HTTP/2 的 Stream 机制天然支持服务端流、客户端流和双向流三种流式 RPC 模式。Protocol Buffers 序列化Protobuf 是 gRPC 默认的 IDL 和序列化方案:二进制编码:相比 JSON 的文本格式,Protobuf 编码体积通常小 3-10 倍,序列化速度快 5 倍以上。原理是采用变长整数(varint)和字段编号而非字段名来标识数据。强类型约束:.proto 文件就是接口契约,编译时就能发现类型不匹配的问题,而不是等到运行时才报错。向后兼容:新增字段只分配新编号,老代码读到未知字段自动跳过,这在微服务滚动升级中至关重要。跨语言代码生成:一个 .proto 文件可以生成 Go、Java、Python、C++ 等 10+ 语言的客户端和服务端代码,保证多语言团队的接口一致性。四种服务模式gRPC 定义了四种通信模式,覆盖从简单请求到实时交互的全部场景:| 模式 | 客户端 | 服务端 | 典型场景 ||------|--------|--------|----------|| 一元 RPC(Unary) | 单个请求 | 单个响应 | 查询用户信息 || 服务端流式 | 单个请求 | 流式响应 | 订阅实时行情 || 客户端流式 | 流式请求 | 单个响应 | 批量上传文件 || 双向流式 | 流式请求 | 流式响应 | 聊天、游戏同步 |为什么选择 gRPC 而不是 REST 或其他 RPC 框架gRPC vs REST| 维度 | gRPC | REST ||------|------|------|| 传输协议 | HTTP/2 | HTTP/1.1 为主 || 数据格式 | Protobuf 二进制 | JSON 文本 || 流式通信 | 四种模式原生支持 | 需 WebSocket 或 SSE || 接口定义 | .proto 文件强约束 | OpenAPI 规范(可选) || 代码生成 | 自动生成多语言桩代码 | 需第三方工具 || 浏览器支持 | 需 gRPC-Web 代理 | 原生支持 |核心结论:内部微服务间通信选 gRPC,对外 API 尤其是面向浏览器/移动端选 REST。两者不是互斥的,很多团队用 gRPC 做内部通信,同时通过 gRPC-Gateway 暴露 REST 接口给外部。gRPC vs ThriftThrift 同样支持二进制序列化和多语言,但 gRPC 的优势在于 HTTP/2 原生支持(Thrift 通常走 TCP 自定义协议)、流式通信、以及 Google 生态(Kubernetes、Envoy 原生支持 gRPC 健康检查和负载均衡)。gRPC 的核心优势性能突出:HTTP/2 多路复用 + Protobuf 二进制编码,端到端延迟通常比 REST+JSON 低 30%-50%,吞吐量提升 5-10 倍。开发效率高:写好 .proto 文件后,一行命令生成所有语言的客户端和服务端代码,接口变更时重新生成即可,无需手工同步。跨语言无缝集成:多语言微服务架构中,Go 写网关、Java 写业务、Python 写算法服务,都用同一份 .proto 定义,类型安全、调用方式统一。流式通信能力:双向流是 gRPC 独特的杀手锏,让实时推送、聊天、监控等场景的实现从 hack 变成标准用法。生态成熟:拦截器(Interceptor)实现鉴权和日志、健康检查协议配合 Kubernetes、内置超时和重试机制、与 OpenTelemetry 链路追踪无缝集成。代码示例定义服务和消息:service OrderService { // 一元调用:查询订单 rpc GetOrder(GetOrderRequest) returns (Order) {} // 服务端流:订阅订单状态变更 rpc SubscribeOrder(SubscribeRequest) returns (stream OrderStatus) {} // 客户端流:批量创建订单 rpc BatchCreateOrders(stream CreateOrderRequest) returns (BatchResult) {} // 双向流:实时议价 rpc Negotiate(stream PriceRequest) returns (stream PriceResponse) {}}message GetOrderRequest { string order_id = 1;}message Order { string order_id = 1; string status = 2; int64 created_at = 3;}Go 语言服务端实现核心逻辑:func (s *Server) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) { order, err := s.repo.FindByID(ctx, req.OrderId) if err != nil { return nil, status.Errorf(codes.NotFound, "order %s not found", req.OrderId) } return order, nil}func (s *Server) SubscribeOrder(req *pb.SubscribeRequest, stream pb.OrderService_SubscribeOrderServer) error { ch := s.eventBus.Subscribe(req.OrderId) for status := range ch { if err := stream.Send(status); err != nil { return err } } return nil}面试追问方向gRPC 的 HTTP/2 多路复用如何避免队头阻塞?和 HTTP/1.1 的队头阻塞有什么本质区别?Protobuf 的 varint 编码原理是什么?负数如何处理?gRPC 的拦截器分哪两种?分别用在什么场景?gRPC-Web 的原理是什么?为什么浏览器不能直接调用 gRPC?生产环境中 gRPC 连接如何做负载均衡?为什么客户端负载均衡更常见?面试中回答 gRPC 问题,关键是把特性说清楚、把和 REST 的对比讲透、把流式通信的场景用实际例子说明,而不是泛泛罗列特性。掌握了 HTTP/2 和 Protobuf 两个底层原理,其他问题都能顺理成章地展开。
服务端阅读 05月27日 23:30

Gradle 有哪些常用命令?构建速度慢怎么优化?

Gradle 是 Android 和 Java 生态的主流构建工具,掌握它的常用命令和构建优化手段是开发者的基本功,也是面试高频考点。最常用的构建命令日常开发中,这几个命令用得最多:# 编译并打包项目./gradlew build# 清理上次构建产物后重新构建./gradlew clean build# 跳过测试加快构建./gradlew build -x test# 只构建 Debug 变体(Android 项目)./gradlew assembleDebug# 只构建 Release 变体./gradlew assembleReleasebuild 会执行编译、测试、打包全流程。如果只想产出 APK/AAB,用 assembleDebug 或 assembleRelease 更快,因为跳过了测试和校验步骤。依赖查看与分析依赖冲突是 Gradle 项目最常见的坑,这几个命令能帮你定位问题:# 查看项目完整依赖树./gradlew dependencies# 只看某个配置的依赖(如 implementation)./gradlew dependencies --configuration implementation# 查看特定模块的依赖./gradlew :app:dependencies# 深入分析某个依赖的来源./gradlew dependencyInsight --dependency gsondependencyInsight 比 dependencies 更实用——它直接告诉你某个库是从哪条路径引入的,在排查版本冲突时效率很高。任务查看与调试# 列出所有可用任务./gradlew tasks# 包括隐藏任务在内全部列出./gradlew tasks --all# 预览任务执行顺序但不真正执行./gradlew build --dry-run# 强制重新执行所有任务(忽略缓存)./gradlew build --rerun-tasks# 查看项目结构./gradlew projects# 查看项目属性./gradlew properties--dry-run 在排查任务依赖关系时很有用,能看到哪些任务会被触发但不会真的执行。构建优化:为什么构建这么慢?Gradle 构建慢,根本原因通常有三个:配置阶段重复执行、任务没有利用缓存、多模块没有并行。下面逐个解决。1. 启用构建缓存构建缓存让 Gradle 跳过输入未变化的任务,直接复用上次的输出:# 命令行临时启用./gradlew build --build-cache永久生效,在 gradle.properties 中配置:org.gradle.caching=true原理:每个任务根据输入内容的哈希值生成缓存键,输入没变就直接取缓存结果。修改一行代码不会导致整个项目重新编译。2. 启用并行执行多模块项目默认串行构建,开启并行后独立模块可以同时编译:# gradle.propertiesorg.gradle.parallel=true# 控制最大并行线程数(默认等于 CPU 核心数)org.gradle.workers.max=4命令行方式:./gradlew build --parallel --max-workers=4注意:只有模块间没有依赖关系的任务才能并行。如果你的模块是线性的依赖链,并行效果有限。3. 启用配置缓存这是 Gradle 8.x 之后最重要的优化。正常每次构建都要执行配置阶段(解析 build.gradle),配置缓存可以在构建脚本没变时直接跳过这一步:# 首次尝试启用(会报告不兼容的地方)./gradlew build --configuration-cache永久配置:org.gradle.configuration-cache=true迁移阶段建议先用 warn 模式:org.gradle.configuration-cache=warn这样构建不会中断,但会在日志里提示哪些代码需要修改才能兼容。Gradle 9.0 已将配置缓存作为默认行为。4. 启用按需配置只配置当前任务涉及的模块,跳过无关模块的配置阶段:org.gradle.configureondemand=true对大型多模块项目(10+ 模块)效果显著,小项目差别不大。5. 调整 JVM 内存Gradle 本身跑在 JVM 上,默认内存可能不够:# gradle.propertiesorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC-Xmx4096m:堆内存,大型项目建议 4GB 以上-XX:MaxMetaspaceSize=1024m:类元数据空间,避免 Metaspace OOM-XX:+UseParallelGC:并行垃圾回收,降低 GC 暂停6. 利用 Gradle DaemonDaemon 是常驻后台的 Gradle 进程,避免每次构建都启动新 JVM:# Daemon 默认已启用,确认状态./gradlew --status# 停止所有 Daemon(出问题时重启)./gradlew --stopDaemon 默认就是开启的,不需要额外配置。如果构建行为异常,先试试 --stop 重启 Daemon。诊断构建瓶颈优化之前先定位瓶颈在哪里:# 生成构建性能报告(HTML)./gradlew build --profile# 生成更详细的 Build Scan(上传到 Gradle 服务器)./gradlew build --scan--profile 会在 build/reports/profile/ 下生成 HTML 报告,按耗时排列各阶段和任务,一眼就能看出哪个任务最耗时。--scan 生成更全面的 Build Scan,包含依赖解析时间、缓存命中率和配置阶段耗时,适合深度排查。一份推荐的 gradle.properties 配置把上面的优化汇总成一份配置,直接复制到项目根目录的 gradle.properties:# 并行构建org.gradle.parallel=true# 构建缓存org.gradle.caching=true# 配置缓存(迁移阶段用 warn)org.gradle.configuration-cache=true# 按需配置org.gradle.configureondemand=true# JVM 内存和 GC 优化org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC# 非 Android 项目可选:开启文件系统监视(加速增量构建)org.gradle.vfs.watch=true对于 Android 项目,还可以在 build.gradle 中开启以下优化:android { // 只构建当前变体,跳过其他变体 variants.all { variant -> if (variant.name != 'debug') { variant.ignore = true } }}常见构建问题与排查命令# 依赖冲突:查看某个库的所有引入路径./gradlew dependencyInsight --dependency 'com.google.code.gson:gson'# 构建失败:查看完整堆栈./gradlew build --stacktrace# 更详细的调试信息./gradlew build --info# 强制刷新依赖(解决缓存损坏)./gradlew build --refresh-dependencies# 离线构建(只用本地缓存,不访问远程仓库)./gradlew build --offline# 持续构建模式(文件变化后自动重新构建)./gradlew build --continuous面试追问方向配置缓存和构建缓存有什么区别? 配置缓存跳过配置阶段(解析 build.gradle),构建缓存跳过执行阶段(任务的输入输出哈希匹配)。两者作用在不同阶段,互不冲突,可以同时开启。并行执行有什么限制? 只有不存在依赖关系的任务才能并行。如果模块 A 依赖模块 B,B 必须先完成。串行依赖链越长,并行收益越低。Gradle Daemon 会不会导致内存泄漏? 长时间运行的 Daemon 确实可能积累内存,Gradle 会在闲置 3 小时后自动停止 Daemon。如果遇到问题,手动 ./gradlew --stop 即可。
服务端阅读 05月27日 23:30

Dubbo 的核心架构是怎样的?服务治理如何实现?

Dubbo 架构的五大角色Dubbo 的架构围绕五个核心角色展开,理解它们之间的协作关系是掌握 Dubbo 的第一步。调用链路:Consumer 发起调用 → 从 Registry 获取 Provider 地址列表 → 通过负载均衡选一台 Provider → Provider 执行并返回结果 → Monitor 记录调用数据。Provider(服务提供者) 暴露服务接口,启动时将自己的地址和元数据注册到 Registry。一个服务可以部署多个 Provider 实例,Consumer 端通过负载均衡策略选择调用哪个实例。Consumer(服务消费者) 从 Registry 订阅所需服务,获取 Provider 列表后缓存在本地。后续调用直接使用本地缓存,即使 Registry 宕机也不影响已有连接。Registry(注册中心) 是服务发现的核心。Dubbo 支持 Zookeeper、Nacos、Redis 等实现,其中 Zookeeper 和 Nacos 是生产环境最常用的选择。Registry 通过长连接推送机制,在 Provider 上线或下线时实时通知 Consumer 更新本地缓存。Monitor(监控中心) 负责统计调用次数和耗时,数据先在内存汇总,每分钟发送一次。Monitor 不参与实际调用链路,宕机不影响服务运行,只丢失采样数据。Container(服务运行容器) 负责启动和加载 Provider,Spring Container 和 Spring Boot 是主流选择。架构的健壮性设计值得注意:注册中心集群对等部署,任意节点宕机自动切换;注册中心全部宕机后,Consumer 仍能通过本地缓存与 Provider 通信;Provider 无状态,单节点宕机不影响整体服务。服务调用与协议选择Dubbo 支持多种通信协议,选择合适的协议直接影响系统性能。Dubbo 协议是默认选项,基于 Netty 的长连接 + NIO 异步传输,采用单一长连接和 Hessian 二进制序列化,适合小数据量高并发的服务间调用。这也是多数生产环境的首选。Triple 卆议是 Dubbo 3.x 推出的新协议,基于 HTTP/2,兼容 gRPC,支持流式通信。如果系统需要跨语言调用或与 gRPC 生态对接,Triple 是更好的选择。其他协议如 HTTP、Hessian、REST 适用于特定场景:HTTP 适合与前端直接交互的网关服务,REST 适合对外暴露 API。调用方式上,Dubbo 支持同步调用、异步调用和泛化调用。异步调用通过 CompletableFuture 实现,适合需要并行调用多个服务的场景;泛化调用不需要 Provider 端的接口定义,适合网关或测试平台这类通用调用方。集群容错策略当 Provider 出现故障时,Dubbo 提供六种容错策略应对不同场景:Failover(失败自动重试) 是默认策略,自动切换到其他 Provider 重试。通过 retries 参数控制重试次数(不含首次调用),默认重试 2 次。适合读操作,写操作需谨慎——重试可能导致数据重复写入。Failfast(快速失败) 只发起一次调用,失败立即报错。适合非幂等的写操作,如创建订单、扣款,避免重试带来的副作用。Failsafe(失败安全) 出现异常时忽略,不抛出异常。适合日志记录、监控上报等非核心操作。Failback(失败自动恢复) 将失败请求记录到后台队列,定时重发。适合消息通知这类最终一致性场景。Forking(并行调用) 同时调用多个 Provider,只要一个成功即返回。通过 forks 参数控制并行数。适合对延迟敏感但资源消耗可以接受的场景。Broadcast(广播调用) 逐个调用所有 Provider,任一失败则报错。适合通知所有节点更新缓存或配置的场景。实际选型建议:读操作用 Failover,写操作用 Failfast,边缘操作用 Failsafe,这是最常见的选择。负载均衡机制Dubbo 内置四种负载均衡策略,核心区别在于请求分发的方式:Random(加权随机) 是默认策略,按权重设置随机概率。在高并发场景下,随机策略的调用分布趋于均匀,且实现简单、性能开销小。RoundRobin(加权轮询) 按权重比例依次轮询分配请求。存在慢请求累积问题——某个 Provider 响应慢时,轮询到它的请求都会阻塞。LeastActive(最少活跃调用数) 优先将请求分配给当前处理中请求数最少的 Provider。这是一种自适应策略,响应越快的 Provider 接收越多请求,能有效避免慢节点堆积。ConsistentHash(一致性哈希) 相同参数的请求总是路由到同一 Provider。当某个 Provider 下线时,其请求会平滑迁移到相邻节点,不会引起大面积重新分配。适合有状态依赖的场景,如用户会话、分片数据。权重调节是线上运维的常用手段:通过 Dubbo Admin 或配置中心动态调整权重,可以实现灰度发布和流量倾斜,无需重启服务。服务治理的核心能力服务治理是 Dubbo 区别于简单 RPC 框架的关键,涵盖降级、限流、路由三大能力。服务降级降级是在 Provider 不可用或响应过慢时,提供兜底方案避免级联故障。Mock 降级是最常用的方式。在 Consumer 端配置 Mock 类,当调用失败时返回预设数据而非抛出异常:// Mock 类命名规则:接口名 + Mockpublic class UserServiceMock implements UserService { @Override public User getUserById(Long id) { return new User(id, "default_user"); }}// 配置方式<dubbo:reference interface="com.example.UserService" mock="true"/>也可以使用 force:return 强制返回指定值,不发起远程调用,用于手动降级。服务限流限流从 Provider 和 Consumer 两个维度控制流量:Provider 端通过 executes 限制每个方法的并发执行数,超出拒绝请求。Consumer 端通过 actives 限制每个服务的并发调用数。Dubbo 3.x 还支持基于 QPS 的限流配置。生产环境中,限流配置通常放在配置中心,根据监控数据动态调整。服务路由路由规则决定请求分发给哪些 Provider,是实现流量控制的核心机制:条件路由是最基础的规则,支持按 IP、应用名、服务名等条件过滤。典型场景:将测试流量路由到灰度机器,线上流量路由到正式机器。标签路由是 Dubbo 3.x 推荐的方式,通过给 Provider 打标签实现流量隔离。例如给灰度机器打上 gray 标签,Consumer 端指定 gray 标签即可将流量路由到灰度环境。脚本路由支持通过 JavaScript 等脚本编写复杂路由逻辑,灵活性最高但维护成本大,生产环境慎用。注册中心与服务发现注册中心的选择直接影响服务发现的稳定性和功能:Zookeeper 是最早支持且使用最广泛的实现,基于树形节点存储服务数据,支持临时节点自动清理下线服务。CAP 模型中偏向 CP,在 Leader 选举期间不可用。适合对一致性要求高的场景。Nacos 是阿里推出的注册中心,同时支持 AP 和 CP 模式切换,内置配置中心功能,与 Spring Cloud 生态兼容。如果项目同时使用 Spring Cloud 和 Dubbo,Nacos 是统一注册中心的最佳选择。服务发现流程:Provider 启动 → 向 Registry 注册 → Consumer 启动 → 从 Registry 订阅 → Registry 推送 Provider 列表 → Consumer 本地缓存并监听变更。关键点是 Consumer 会缓存 Provider 列表,Registry 推送变更时增量更新,即使 Registry 全部宕机,Consumer 仍能通过本地缓存调用 Provider。配置中心的作用Dubbo 3.x 将配置中心独立出来,与注册中心解耦。配置中心负责外部化配置管理、动态配置推送和配置版本管理,支持 Nacos、Zookeeper、Apollo 等实现。动态配置是配置中心的核心价值。修改服务超时时间、负载均衡策略、权重等参数后,配置中心实时推送到所有节点,无需重启服务。这在处理线上问题时非常关键——某个服务响应变慢,可以立即调大超时时间而不是等待发布。面试追问方向Q:Dubbo 和 Spring Cloud 如何选型?Dubbo 专注 RPC 通信和服务治理,性能优于 Spring Cloud 的 HTTP 通信;Spring Cloud 提供更完整的微服务解决方案(网关、配置、链路追踪等)。内部服务间调用选 Dubbo,需要完整微服务栈选 Spring Cloud,两者也可以通过 Nacos 共存。Q:Dubbo 3.x 相比 2.x 有哪些重大变化?Triple 协议替代 Dubbo 协议成为推荐协议;应用级服务发现替代接口级服务发现,减少注册中心压力;服务路由引入标签路由作为推荐方案。理解这些变化有助于理解 Dubbo 的演进方向。
服务端阅读 05月27日 23:26

什么是服务注册与发现?注册中心如何选型?

答案服务注册与发现是微服务中解决"实例动态管理"的核心机制:服务注册是实例启动时将地址信息写入注册中心,服务发现是调用方从注册中心拉取可用实例列表。注册中心是两者的协调中介,同时负责健康检查和故障剔除。主流注册中心按 CAP 模型分两派:AP 派:Eureka — 自我保护机制,网络分区时保留过期数据仍可查询,但可能拿到已下线实例。Spring Cloud 集成好,2.x 已停维。CP 派:Zookeeper(ZAB)、Etcd(Raft)、Consul(Raft) — 主节点宕机时拒绝写入直到选主完成,牺牲可用性换一致性。混合派:Nacos — 临时实例走 AP(Distro 协议),持久化实例走 CP(Raft 协议),是国内微服务首选。追问一:Eureka 自我保护机制?心跳续约比例低于阈值(默认85%)时进入自我保护:不再剔除过期实例。防止网络抖动导致误剔除,代价是可能调用到已死实例,需配合重试和熔断。追问二:Nacos 如何实现 AP/CP 切换?临时实例(ephemeral)用 Distro 协议(AP),节点平等写入并异步同步;持久化实例(persistent)用 Raft 协议(CP),写入需多数派确认。无状态服务用 AP,有状态服务用 CP。追问三:客户端发现 vs 服务端发现?客户端发现:调用方自己拉取实例列表做负载均衡(Eureka/Nacos),少一跳但客户端逻辑重。服务端发现:请求先到代理(K8s Service/Nginx)再转发,客户端无感知但多一跳延迟。选型速判Spring Cloud Alibaba → Nacos;K8s → Etcd + CoreDNS;多数据中心 + 服务网格 → Consul;强一致性 → Zookeeper。Eureka 仅适合已有 Netflix 栈项目维护。# Nacos 临时实例配置(AP 模式)spring: cloud: nacos: discovery: server-addr: localhost:8848 namespace: dev # ephemeral: true # 默认 true,即 AP 模式# Nacos 持久化实例配置(CP 模式)spring: cloud: nacos: discovery: server-addr: localhost:8848 ephemeral: false # 切换为 CP 模式
服务端阅读 05月27日 23:24

RPC 调用中的容错机制有哪些?

核心容错策略有哪些?RPC 调用面临网络抖动、服务宕机、过载等故障,容错机制围绕快速失败和优雅降级两个原则展开,主要包括超时、重试、熔断、限流、降级五种策略。超时与重试:第一道防线超时防止调用方无限阻塞。需区分连接超时和读取超时,根据 P99 延迟动态调整。Dubbo 的 timeout、gRPC 的 deadline 是典型实现。重试应对临时性故障(网络抖动、GC 停顿),但必须满足两个前提:接口幂等:重复调用不产生副作用,可通过唯一请求 ID 保证退避策略:指数退避(1s → 2s → 4s)避免重试风暴// Dubbo 重试配置示例@DubboReference(retries = 2, timeout = 3000)private UserService userService; 面试追问:非幂等接口(如下单)如何重试?——不重试,改用异步确认或 TCC 补偿。熔断:防止故障雪崩当下游故障率超过阈值,熔断器主动切断调用,直接返回失败,避免大量请求堆积拖垮上游。三个状态转换:Closed → 正常调用,统计失败率Open → 失败率超阈值,快速失败,不发起调用Half-Open → 经过恢复时间窗口后,放少量请求探测,成功则回 Closed,失败则回 Open实现:Hystrix(已停更)、Resilience4j、Sentinel。 关键参数:失败率阈值(如 50%)、超时时间、恢复窗口。限流与降级:保护与兜底限流从入口控制流量,核心算法对比:| 算法 | 特点 | 适用场景 ||------|------|----------|| 令牌桶 | 允许突发流量,匀速生成令牌 | 一般业务 || 漏桶 | 严格匀速输出 | 流量整形 || 滑动窗口 | 精确统计窗口内请求数 | 精确限流 |降级在服务不可用时提供兜底方案:返回缓存数据、默认值或简化逻辑。与熔断的区别——熔断是切断调用,降级是提供替代结果,两者常配合使用。如何组合使用?生产环境通常多层组合:入口限流 → 防止流量冲击调用超时 + 重试 → 应对临时故障熔断 + 降级 → 防止级联失败隔离(线程池/信号量)→ 防止单服务耗尽资源按业务重要性分级配置:核心链路(支付)用严格熔断+快速降级;非核心链路(推荐)可放宽重试、允许失败。
服务端阅读 05月27日 23:24

什么是服务治理?RPC 框架如何实现服务治理?

答案前置服务治理是微服务架构中保障服务稳定、可控运行的一整套机制。RPC 框架中的服务治理核心包括:服务注册与发现、负载均衡、容错降级、限流熔断、路由与灰度、监控与配置管理六大能力。其本质是在远程调用链路上,加入"发现谁能调、怎么调更优、调失败了怎么办、流量怎么控"的决策逻辑。服务注册与发现服务提供者启动时将自身地址注册到注册中心(Nacos、ZooKeeper、Consul),消费者从注册中心订阅地址列表。核心要解决的是实例上下线的实时感知:通过心跳检测剔除不健康实例,通过推送或拉取机制更新地址列表。// Dubbo 注册配置<dubbo:registry address="nacos://127.0.0.1:8848"/>负载均衡与路由拿到地址列表后,需要决定请求打到哪个实例。常见算法有随机、轮询、最少连接、一致性哈希。路由则在负载均衡之上增加条件过滤,比如灰度发布时按标签将流量导向新版本实例。<dubbo:reference loadbalance="consistenthash"/><dubbo:router rule="host = 10.0.0.* => provider.version = 2.0"/>追问:一致性哈希为什么适合有状态服务? 因为同一 key 始终路由到同一节点,避免状态迁移。容错、降级与熔断这三者常被混淆,但解决的问题不同:容错:调用失败后的重试策略(Failover 重试其他实例、Failfast 快速失败)降级:服务不可用时返回兜底结果,保证主流程不中断熔断:当错误率超过阈值,直接切断调用,避免级联故障(雪崩)熔断器有三个状态:Closed(正常)→ Open(熔断,快速失败)→ Half-Open(试探恢复)。// Sentinel 熔断降级@SentinelResource(value = "getUser", fallback = "fallback", blockHandler = "handleBlock")public User getUser(Long id) { return userService.getUser(id); }public User fallback(Long id) { return new User(id, "默认用户"); }限流保护服务不被突发流量压垮。令牌桶允许突发流量,漏桶强制匀速,滑动窗口统计更精准。实际选型看场景:对外网关用令牌桶,内部服务用滑动窗口。RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个令牌if (rateLimiter.tryAcquire()) { /* 处理请求 */ }监控与配置监控是治理的眼睛:QPS、RT、错误率是核心指标,Prometheus + Grafana 是主流方案。配置中心(Nacos Config、Apollo)支持动态下发,无需重启即可调整超时、限流阈值等参数。总结服务治理的各能力不是孤立的:注册发现是基础,负载均衡和路由决定流量去向,限流熔断降级保障稳定性,监控配置让整个系统可观测可调整。面试中重点讲清楚三者区别(容错 vs 降级 vs 熔断)和选型思路,比罗列功能更有价值。
服务端阅读 05月27日 23:23

如何实现 RPC 的异步调用?

答案RPC 异步调用的核心思路是:客户端发起调用后不阻塞等待响应,而是通过 Future、回调或响应式流等机制在结果就绪时获取。常见有三种模式:Future/Promise — 调用立即返回 Future 对象,调用方自行决定何时获取结果。Dubbo 原生支持 async=true,通过 RpcContext.getFuture() 拿到返回值。Callback 回调 — 传入回调接口,服务端响应到达时自动触发。适合事件驱动场景,但多层嵌套易产生回调地狱。响应式流(Reactive) — 基于 Reactor/RxJava,以 Mono/Observable 表示异步结果,支持背压和链式组合,适合流式处理。此外,Java 8 的 CompletableFuture 兼具 Future 和回调的优点,支持 thenCombine 组合多个异步结果,是目前最常用的异步编排工具。gRPC 则通过 StreamObserver 实现异步,并原生支持双向流通信。异步调用的优势不阻塞调用线程,单线程可同时处理多个请求,提高并发能力和吞吐量并行调用多个服务,用 CompletableFuture.allOf 或响应式 zip 组合结果,显著降低总延迟避免资源浪费,线程不必在 I/O 等待上空转关键挑战上下文传递:异步线程切换时 TraceId、用户信息等上下文易丢失,需用 TransmittableThreadLocal 显式传递超时控制:必须设超时并取消,否则请求可能无限挂起。future.get(timeout) + future.cancel(true) 是基本模式线程池管理:异步任务不能无限制创建线程,需配置有界线程池并监控队列积压追问Q: 同步调用和异步调用怎么选?调用方需要立即拿到结果才能继续(如下单扣库存),用同步;调用方不依赖结果或可以后续处理(如发通知、写日志),用异步。实际中大部分 RPC 调用用同步,并行调用多个服务时用异步组合。Q: CompletableFuture 和 Reactor 的区别是什么?CompletableFuture 处理单值异步结果,API 简单,适合多数业务场景;Reactor 基于响应式流规范,支持多值序列和背压,适合流式数据处理,但学习成本更高。如果只是组合几个 RPC 调用,CompletableFuture 足够。Q: 异步调用失败怎么重试?不要简单循环重试,应采用指数退避策略(如初始 100ms,每次翻倍,最多 3 次),配合熔断器在连续失败时快速失败。CompletableFuture 可用 exceptionally 或 handle 捕获异常后触发重试逻辑。
服务端阅读 05月27日 23:23

RPC 调用如何保证安全性?认证、加密与授权怎么做?

RPC 调用走的是网络,任何中间人都能截获、篡改甚至伪造请求,所以安全性不是可选项,而是必选项。核心要解决三个问题:你是谁(认证)、数据别被偷看(加密)、你能干什么(授权)。身份认证:确认调用方身份最常见的是 Token 认证,客户端每次请求携带 JWT 或 OAuth2 Token,服务端校验后放行。gRPC 中通常用拦截器统一拦截:public class AuthInterceptor implements ServerInterceptor { @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { String token = headers.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); if (!validateToken(token)) { call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), headers); return new ServerCall.Listener<ReqT>() {}; } return next.startCall(call, headers); }}内部服务间调用更简单的做法是 API Key,给每个服务分配固定密钥,缺点是一旦泄露很难快速更换。安全性要求高的场景用双向 TLS(mTLS):客户端和服务端互相验证证书,只有持有合法证书的才能通信,即使 Token 被盗也无法伪造连接。数据加密:防止传输中被窃听和篡改传输层加密是第一道防线。所有 RPC 框架都支持 TLS,gRPC 配置示例:NettyChannelBuilder.forAddress(host, port) .sslContext(GrpcSslContexts.forClient() .trustManager(new File("ca.pem")) .build()) .build();仅靠 TLS 不够,敏感字段还应做应用层加密(AES/RSA),这样即便 TLS 被中间人攻破,核心数据仍有保护。完整性校验用 HMAC 或数字签名。发送方对请求体计算签名,接收方验证签名是否一致,能发现任何篡改行为。授权:控制调用方能访问什么认证通过后还要判断有没有权限。最实用的是 RBAC,给服务或用户分配角色,角色绑定权限集合。也可以做到方法级别的细粒度控制,比如只有 admin 角色才能调用 DeleteUser。实际项目中通常用注解 + 拦截器的方式:@RequireRole("admin")public void deleteUser(UserRequest req) { ... }拦截器在调用前统一校验角色,业务代码无需关心权限逻辑。防重放和限流重放攻击是拿合法请求重复发送。解法是请求中加时间戳 + Nonce(一次性随机数),服务端校验时间窗口内的请求是否重复。再加上请求签名,把时间戳、Nonce 和参数一起签名,篡改任何一项都会验签失败。限流用令牌桶算法,防止某个调用方吃满资源:RateLimiter rateLimiter = RateLimiter.create(100);if (!rateLimiter.tryAcquire()) { throw new RateLimitExceededException();}配合 IP 黑白名单,可疑来源直接拦截。安全实践要点最小权限:服务只开通必需的调用权限密钥轮换:定期更换 Token、证书,用配置中心管理而非硬编码审计日志:记录调用方、时间、参数,异常模式及时告警框架差异:gRPC 原生支持 TLS + 拦截器;Dubbo 用 Filter 扩展;Thrift 用 TSSLTransport追问:mTLS 和普通 TLS 的区别?普通 TLS 只验证服务端证书,客户端不提供证书;mTLS 要求双方都提供证书互相验证,安全性更高但证书管理成本也更大,适合零信任网络架构。
服务端阅读 05月27日 23:23

什么是 RPC?RPC 的基本原理和工作流程是什么?

RPC 是什么?RPC(Remote Procedure Call)让一个进程像调用本地函数一样调用远端进程的函数,调用方无需关心底层网络细节。简单说,你写 result = addUser(user) 的时候,addUser 可能跑在另一台机器上,但你代码看起来和本地调用没区别。一次 RPC 调用经历了什么?以调用 userServer.getUser(id) 为例:客户端调用 Stub:调用方发起调用,实际先到客户端存根(Stub),Stub 负责把方法名、参数打包序列化:将参数对象转成二进制流(如 Protobuf、Hessian),这是"编组"过程网络传输:二进制数据通过 TCP/HTTP2 发往服务端,gRPC 默认走 HTTP/2服务端反序列化:Skeleton 接收数据,还原出方法名和参数执行本地方法:服务端找到真实实现类执行,拿到返回值结果序列化回传:返回值同样序列化后走网络回客户端客户端反序列化:Stub 把二进制还原成结果对象,返回给调用方整个过程对业务代码透明,网络通信、序列化、寻址全部由框架处理。核心组件Stub/Skeleton:客户端和服务端的代理层,屏蔽网络细节序列化协议:Protobuf 体积最小性能最好,JSON 通用但空间开销大,Hessian 折中传输协议:TCP 直连延迟最低,HTTP/2 支持多路复用,gRPC 底层用 HTTP/2 + Protobuf注册中心:服务端注册地址,客户端从注册中心发现服务(Consul、Etcd、Nacos)为什么不直接用 HTTP?HTTP 1.1 是文本协议,头部冗余大,每次请求都要建连接;RPC 框架通常基于长连接 + 自定义二进制协议,序列化体积小、连接复用,吞吐量高出一个量级。当然 gRPC 基于 HTTP/2 也能兼顾性能和通用性,具体选型看场景。常见追问RPC 和本地调用有什么区别?本地调用可靠且零延迟;RPC 存在网络抖动、超时、服务不可用等问题,需要重试、熔断、降级等容错机制。序列化怎么选?对性能敏感选 Protobuf,跨语言交互且可读性优先选 JSON,Java 体系内 Dubbo 默认 Hessian 也够用。服务挂了怎么办?注册中心心跳检测摘除异常节点,客户端侧配合熔断器(如 Sentinel)快速失败,避免级联雪崩。
服务端阅读 05月27日 23:23

主流 RPC 框架 gRPC、Dubbo、Thrift 该怎么选?

核心结论选 RPC 框架先定语言生态,再看服务治理需求,最后看性能瓶颈:Java 项目优先 Dubbo,跨语言项目优先 gRPC,遗留系统多协议兼容考虑 Thrift。三个框架的本质区别选型之前,先搞清楚它们在通信模型和序列化机制上的根本差异,这决定了它们的性能天花板和适用边界。通信协议不同:gRPC 基于 HTTP/2,天然支持多路复用、流式通信、头部压缩Dubbo 默认走 TCP 长连接 + 自定义协议,单连接上传输更紧凑Thrift 支持多种传输方式(TSocket、THttp、TFramed),灵活但需要自己选序列化机制不同:gRPC 用 Protobuf,强类型 + 二进制,序列化体积最小,但需要 .proto 文件编译生成代码Dubbo 默认 Hessian2,支持多序列化切换(Kryo、Protobuf 等),Java 原生兼容好Thrift 用自己的 IDL 生成代码,支持 Binary/Compact/JSON 多种格式按场景选型Java 单栈微服务 → DubboDubbo 的核心优势不在性能,而在服务治理体系的完整度:内置注册中心(ZooKeeper/Nacos)、负载均衡策略(随机/轮询/一致性哈希)、熔断降级、服务分组和版本控制。Spring Cloud Alibaba 生态下,Dubbo 与 Nacos、Sentinel 的整合几乎是零成本。如果团队全是 Java 栈,选 Dubbo 省的是运维和治理的成本。注意:Dubbo 3.x 已经支持 Triple 协议(基于 HTTP/2),可以和 gRPC 互通,跨语言能力在补齐。跨语言微服务 → gRPCgRPC 的跨语言是第一等公民:一个 .proto 文件生成 Go、Java、Python、C++ 等十几种语言的客户端和服务端代码,接口定义即契约。HTTP/2 带来的流式通信(客户端流、服务端流、双向流)是 Dubbo 和 Thrift 不原生支持的特性,适合实时数据推送、大文件分片传输等场景。代价是调试不方便——二进制协议无法直接用 curl 打,需要 grpcurl 或 grpc-web 做代理;服务治理需要自己搭(配合 Consul/etcd 做服务发现,配合 Jaeger 做链路追踪)。多协议兼容 / 遗留系统 → ThriftThrift 最大的价值是灵活性:传输层可选 TSocket/THttp/TFramed,协议层可选 TBinary/TCompact/TJSON,序列化格式可以混搭。对于既有 C++ 老系统又要对接 Java 新服务的场景,Thrift 的多协议支持比 gRPC 的单一 Protobuf 更容易兼容。但社区活跃度不如 gRPC 和 Dubbo,遇到问题排查成本高。性能数据参考同一环境下的 QPS 对比(小型请求体,单连接):gRPC (Protobuf):约 35k QPSDubbo (Hessian2):约 28k QPSThrift (TBinary):约 30k QPSFeign (JSON/HTTP1.1):约 12k QPS差距在日常业务中感知不大,瓶颈通常在数据库和 IO 而非框架本身。不要为了 20% 的框架性能差异牺牲 50% 的开发效率。面试追问方向Dubbo 的服务降级和熔断怎么实现?(Sentinel 集成 + mock 机制)gRPC 的流式通信在什么业务场景下不可替代?Protobuf 和 JSON 序列化的性能差距在什么量级?什么场景下 JSON 更合适?Dubbo 3.x 的 Triple 协议解决了什么问题?
服务端阅读 05月27日 23:21

Gradle 是什么?它有哪些核心概念和优势?

Gradle 是一个基于 JVM 的构建自动化工具,融合了 Ant 的灵活性与 Maven 的约定优于配置理念,使用 Groovy 或 Kotlin DSL 编写构建脚本。它的核心概念围绕 Project、Task、Plugin 三层展开。Project每个 Gradle 构建由一个或多个 Project 组成。单模块项目只有一个 Project,多模块项目在 settings.gradle.kts 中声明子项目。每个 Project 对应一个 build.gradle.kts 脚本,是配置的顶级容器。TaskTask 是 Gradle 执行的最小单元,如编译、测试、打包。Task 之间通过 dependsOn 声明依赖关系,Gradle 据此生成有向无环图(DAG),按拓扑排序执行:tasks.register("copyFiles") { dependsOn("processResources") doLast { copy { from("build/resources") into("dist") } }}关键点:Task 分为配置阶段和执行阶段,doFirst / doLast 中的逻辑只在执行阶段运行。PluginPlugin 是功能扩展的核心机制。二进制插件通过 plugin ID 应用:plugins { id("org.springframework.boot") version "3.2.0"}插件内部通过注册 Task、扩展 DSL、配置依赖来改变构建行为。Java Plugin、Android Plugin 都是这个模式。构建生命周期Gradle 构建分三个阶段:初始化:执行 settings.gradle.kts,确定参与构建的 Project配置:执行所有 build.gradle.kts,构建 Task 依赖图执行:按 DAG 顺序执行目标 Task 及其依赖理解生命周期是排查构建问题的关键——配置阶段的代码每次构建都会执行,即使只运行一个 Task。依赖管理Gradle 支持 Maven、Ivy 仓库和本地文件依赖,提供 resolutionStrategy 处理版本冲突,默认取最新版本。implementation 与 api 的区别是常见追问点:implementation 依赖不传递,api 依赖会传递到上层模块。Gradle Wrapper项目自带 gradlew 脚本,自动下载指定版本的 Gradle,保证团队构建环境一致。无需手动安装 Gradle 即可构建项目。核心优势增量构建:只重新执行输入输出有变化的 Task构建缓存:跨项目复用相同 Task 的输出并行执行:多模块并行构建,利用多核 CPU这三项机制叠加,使 Gradle 在大型项目中的构建速度远超 Maven。常见追问Gradle 和 Maven 的核心区别? Maven 基于 XML 声明式配置,逻辑固定在插件中;Gradle 用 DSL 编程式定义构建逻辑,灵活度更高,且增量构建和缓存机制使其速度更快。implementation 和 api 有什么区别? implementation 的依赖对上层模块不可见,修改时不触发上层重新编译;api 的依赖会传递,适合暴露给消费者的公共 API 依赖。
服务端阅读 05月27日 23:20

Gradle 中的 Task 是什么?如何创建和配置 Task?

核心答案Task 是 Gradle 构建的最小执行单元,代表一个原子操作(编译、打包、测试等)。每个 Task 包含名称、类型、动作(Action)、依赖关系和输入/输出。创建 Task 有三种方式,推荐使用 tasks.register() 实现延迟初始化,避免配置阶段不必要的开销。创建 Taskregister vs create vs task 关键字// 推荐:延迟创建,仅在实际需要时实例化tasks.register('myTask') { doLast { println 'Executing myTask' }}// 立即创建,配置阶段就会实例化tasks.create('myTask') { doLast { println 'Executing myTask' }}// DSL 快捷方式,等同 createtask myTask { doLast { println 'Executing myTask' }}register 与 create 的核心区别在于:register 返回 TaskProvider,Task 实例在首次被引用时才创建。大型项目中大量使用 create 会导致配置阶段耗时增加,register 可显著改善构建性能。指定 Task 类型tasks.register('copyFiles', Copy) { from 'src/main/resources' into 'build/resources'}继承内置类型(Copy、Delete、Exec 等)可以复用已有的文件操作逻辑,不必从零实现。配置 Tasktasks.register('myTask') { group = 'Custom Tasks' // Gradle 面板中的分组 description = '自定义任务描述' // gradle tasks 时的说明 dependsOn 'clean', 'compileJava' // 依赖其他任务 mustRunAfter 'test' // 硬排序约束 shouldRunAfter 'build' // 软排序约束(可被 Gradle 忽略) inputs.file('config.properties') // 声明输入 outputs.dir('build/output') // 声明输出 doFirst { println '执行前' } doLast { println '执行后' }}声明 inputs/outputs 的意义在于启用增量构建:Gradle 检测到输入输出未变化时会跳过 Task 执行,这是提升构建速度的关键机制。依赖与排序dependsOn:声明必须先完成的依赖,Gradle 据此构建有向无环图(DAG)决定执行顺序mustRunAfter:硬约束,两个任务同时被调度时保证先后顺序,但不引入依赖shouldRunAfter:软约束,Gradle 在并行执行优化时可以忽略finalizedBy:无论当前任务成功或失败,都会执行收尾任务(如资源清理)tasks.register('taskA') { finalizedBy 'cleanup' doLast { println 'Task A' }}tasks.register('cleanup') { doLast { println 'Cleanup' }}执行控制// 条件执行tasks.register('conditionalTask') { onlyIf { project.hasProperty('runConditional') } doLast { println '条件满足才执行' }}// 禁用任务tasks.register('disabledTask') { enabled = false}// 抛出 StopExecutionException 跳过当前动作doFirst { if (!project.hasProperty('force')) { throw new StopExecutionException() }}追问register 创建的 Task 如何在其他地方引用? 使用 tasks.named('myTask') 获取 TaskProvider,再通过 .configure { } 追加配置。不要对 register 的 Task 直接用 tasks.myTask,那会触发立即实例化,失去延迟初始化的优势。mustRunAfter 和 dependsOn 有什么区别? dependsOn 会建立真正的依赖关系,被依赖的任务一定会执行;mustRunAfter 只是排序约束,仅当两个任务都在本次构建的执行计划中时才生效,不会把对方拉入执行图。为什么不要在 Task 体中直接写逻辑而要用 doFirst/doLast? Task 闭包中的代码在配置阶段执行,而 doFirst/doLast 中的代码在执行阶段执行。配置阶段写逻辑会导致每次运行任何 Task 都会执行这些代码,产生副作用和性能浪费。
服务端阅读 05月27日 23:19

Gradle 的增量构建是如何工作的?

核心机制Gradle 增量构建通过跟踪任务的输入(inputs)和输出(outputs)的哈希值来决定是否跳过执行。当输入未变化且输出存在时,任务标记为 UP-TO-DATE 并跳过;只有输入变化或输出缺失时才重新执行。关键点:首次构建记录所有输入输出的快照(哈希值)后续构建比较当前快照与历史快照增量任务可进一步获取文件级变更细节(新增/修改/删除),只处理变化部分输入输出声明没有正确声明输入输出的任务无法参与增量构建:abstract class ProcessTask extends DefaultTask { @InputDirectory abstract DirectoryProperty getInputDir() @OutputDirectory abstract DirectoryProperty getOutputDir() @TaskAction void execute(InputChanges changes) { if (changes.incremental) { changes.getFileChanges(inputDir).each { change -> // change.changeType: ADDED / MODIFIED / REMOVED // 只处理变化的文件 } } else { // 全量处理 } }}运行时 API 方式(无需自定义 Task 类):tasks.register('processFiles') { inputs.dir('src/main/resources') outputs.dir('build/processed') doLast { /* ... */ }}构建性能优化手段1. 启用增量编译与构建缓存# gradle.propertiesorg.gradle.caching=trueorg.gradle.parallel=true2. 远程构建缓存(CI 环境)buildCache { remote(HttpBuildCache) { url = 'https://cache.example.com/cache/' push = System.getenv('CI') != null }}3. 配置缓存(Configuration Cache):缓存项目配置阶段结果,避免每次构建重新解析 build.gradle,Gradle 8.x 已趋于稳定。4. 避免配置阶段耗时操作:将逻辑移入 doLast / doFirst,而非任务配置闭包中。常见增量构建失效的原因输入输出未声明或声明不全 → 任务每次都执行输出被外部进程修改 → 缓存校验失败任务存在非确定性输出(如含时间戳) → 同样输入产生不同输出outputs.upToDateWhen 中包含非确定性逻辑调试命令:./gradlew build --info # 查看 UP-TO-DATE 原因./gradlew build --rerun-tasks # 强制全量执行./gradlew build --scan # 生成构建分析报告追问Q: 增量构建和构建缓存有什么区别?增量构建在同一项目内比较输入输出快照跳过任务;构建缓存可以跨项目、跨机器复用任务输出,存储在本地或远程。两者互补:增量构建避免本地重复执行,构建缓存避免跨环境重复执行。Q: 输入归一化(Input Normalization)是什么?Gradle 比较输入时会对 classpath 做归一化处理——忽略 JAR 中的时间戳和顺序差异,只比较实际内容。可通过 @Classpath 和 @CompileClasspath 注解控制归一化策略,减少不必要的任务重新执行。Q: 配置缓存对增量构建有什么影响?配置缓存缓存的是配置阶段的结果(任务图、属性值),增量构建缓存的是执行阶段的输入输出快照。两者作用于不同阶段,同时启用效果叠加。但配置缓存要求构建脚本无副作用,部分插件可能不兼容。
服务端阅读 05月27日 23:18

Cypress 的 beforeEach、before、afterEach 和 after 钩子有什么区别?

核心区别四个钩子的根本区别在于执行频率和作用域:beforeEach / afterEach:每个 it 用例前后各执行一次,作用域为当前 describe 块内所有用例before / after:整个 describe 块开始前和结束后各执行一次,作用域为整个测试套件| 钩子 | 执行时机 | 执行次数 | 典型用途 ||------|---------|---------|--------|| beforeEach | 每个 it 之前 | N 次(N = 用例数) | 重置状态、登录、访问页面 || afterEach | 每个 it 之后 | N 次 | 清除 cookie、会话、快照 || before | 所有 it 之前 | 1 次 | 种子数据、全局配置 || after | 所有 it 之后 | 1 次 | 数据库清理、资源释放 |执行顺序嵌套 describe 时,钩子的执行顺序遵循"从外到内"原则:describe("外层", () => { before(() => cy.log("outer before")); // 1 beforeEach(() => cy.log("outer beforeEach")); // 3, 7 describe("内层", () => { before(() => cy.log("inner before")); // 2 beforeEach(() => cy.log("inner beforeEach")); // 4, 8 it("测试A", () => cy.log("test A")); // 5 it("测试B", () => cy.log("test B")); // 9 afterEach(() => cy.log("inner afterEach")); // 6, 10 }); afterEach(() => cy.log("outer afterEach")); // 最后执行});输出:outer before → inner before → (outer beforeEach → inner beforeEach → 测试A → inner afterEach → outer afterEach) × 2轮选择依据需要每个用例都从干净状态开始 → beforeEach,不要用 before用例之间允许共享状态(如只读数据) → before 一次性初始化afterEach 适合清理当前用例产生的副作用(如 localStorage)after 适合清理整个套件的资源(如测试数据库)常见错误:在 before 中登录,然后所有用例共享登录态。一旦某个用例意外注销,后续用例全部失败。正确做法是用 beforeEach 登录,保证每个用例的独立性。追问Q: beforeEach 中 cy.visit() 和 cy.request() 有什么区别?cy.visit() 会加载完整页面并等待页面事件,较慢;cy.request() 只发 HTTP 请求不渲染,适合用 API 预设数据来加速测试。Q: 钩子中的断言失败会影响用例执行吗?会。beforeEach 中断言失败,该用例跳过;afterEach 中失败会标记用例为失败,但不影响下一个用例的执行。Q: 为什么 Cypress 官方更推荐 beforeEach 而非 before?因为 Cypress 的核心原则是测试隔离。before 共享状态容易导致用例间耦合,一个用例的副作用会污染后续用例。beforeEach 保证每个用例从相同初始状态运行,测试更稳定。
服务端阅读 05月27日 23:18

Gradle 插件有哪些类型?如何创建和使用自定义插件?

核心回答Gradle 插件分两类:脚本插件和二进制插件。创建自定义插件有三种方式:在 build script 中直接编写、在 buildSrc 模块中组织、以及创建独立项目发布。插件类型脚本插件是额外的 .gradle 文件,通过 apply from 引入:apply from: 'gradle/checkstyle.gradle'只能在本项目复用,不推荐用于复杂逻辑。二进制插件实现了 Plugin<Project> 接口,打包为 JAR 后可跨项目共享:plugins { id 'java' // 核心插件,无需版本 id 'org.springframework.boot' version '3.0.0' // 社区插件,需指定版本}推荐使用 plugins DSL 而非旧式 apply plugin:。创建自定义插件的三种方式1. Build Script 内联直接在 build.gradle 中编写,适合一次性逻辑:class GreetingPlugin implements Plugin<Project> { void apply(Project project) { project.tasks.register('greet') { doLast { println 'Hello from plugin' } } }}apply plugin: GreetingPlugin无法在其他项目复用。2. buildSrc 模块在项目根目录创建 buildSrc/src/main/groovy/ 目录,插件类放于此。整个项目可见,可编写测试,但不能跨项目共享。3. 独立项目(推荐)创建独立的 Gradle 插件项目,发布到 Maven 仓库供任意项目使用:// 插件项目 build.gradleplugins { id 'java-gradle-plugin' id 'maven-publish'}gradlePlugin { plugins { customPlugin { id = 'com.example.custom-plugin' implementationClass = 'com.example.CustomPlugin' } }}插件实现类:package com.exampleclass CustomPlugin implements Plugin<Project> { void apply(Project project) { def ext = project.extensions.create('customConfig', CustomExtension) project.tasks.register('customTask') { group = 'Custom' doLast { println "Config: ${ext.name}" } } }}class CustomExtension { String name = 'default'}注册插件 ID(META-INF/gradle-plugins/com.example.custom-plugin.properties):implementation-class=com.example.CustomPlugin使用方引入:plugins { id 'com.example.custom-plugin'}customConfig { name = 'MyApp'}追问plugins DSL 与 apply 有什么区别? plugins DSL 在配置阶段解析,支持类型安全和版本管理;apply 在脚本执行时才应用,无法提前校验。自定义插件如何测试? 使用 Gradle TestKit 编写功能测试,在临时项目中运行构建验证行为。为什么推荐用 Java/Kotlin 而非 Groovy 实现插件? 静态类型减少二进制不兼容风险,Kotlin DSL 还能获得 IDE 补全支持。
服务端阅读 05月27日 23:18

Cypress 自定义命令怎么用?

Cypress 自定义命令(Custom Commands)是通过 Cypress.Commands.add() 在 cypress/support/commands.js 中注册的可复用测试函数,调用方式与 cy.visit() 等内置命令一致,核心目的是消除跨用例的重复代码。创建自定义命令在 cypress/support/commands.js 中定义:Cypress.Commands.add('login', (email, password) => { cy.visit('/login'); cy.get('[data-testid="email"]').type(email); cy.get('[data-testid="password"]').type(password); cy.get('[data-testid="submit"]').click();});测试中直接调用 cy.login('user@example.com', 'password'),无需每次重复编写登录步骤。三种命令类型Cypress.Commands.add() 第二个参数可选 prevSubject,决定命令的调用方式:父命令(默认):独立调用,如 cy.login()子命令:必须链式接在前一个命令后,对获取到的元素操作Cypress.Commands.add('drag', { prevSubject: 'element' }, (subject, options) => { cy.wrap(subject) .trigger('mousedown', { button: 0 }) .trigger('mousemove', { clientX: options.x, clientY: options.y }) .trigger('mouseup');});// 使用:cy.get('.box').drag({ x: 100, y: 200 })双重命令:{ prevSubject: 'optional' },既可独立调用也可链式调用覆盖已有命令用 Cypress.Commands.overwrite() 改写内置命令行为:Cypress.Commands.overwrite('visit', (originalFn, url, options) => { return originalFn(url, { ...options, headers: { Authorization: 'Bearer ...' } });});TypeScript 类型支持在 cypress/support/index.d.ts 中声明类型,避免 TS 报错:declare namespace Cypress { interface Chainable { login(email: string, password: string): Chainable<void>; drag(options: { x: number; y: number }): Chainable<void>; }}常见追问自定义命令和普通函数的区别? 自定义命令运行在 Cypress 命令队列中,支持重试和超时机制;普通 JS 函数是同步执行,不具备这些能力。什么时候不该用自定义命令? 仅在单个 spec 文件中复用的逻辑,写成普通函数更轻量;自定义命令适合跨文件、跨模块共享的场景。命令命名冲突怎么办? 自定义命令会覆盖同名内置命令,建议用业务前缀(如 cy.authLogin)避免冲突。
服务端阅读 05月27日 23:17

Cypress 如何处理跨域问题?

答案Cypress 处理跨域问题有两种主要方式:禁用 Chrome Web 安全:在 cypress.config.js 中设置 chromeWebSecurity: false,允许跨域导航和访问跨域 iframe。这是最简单的方式,但仅适用于 Chromium 内核浏览器。使用 cy.origin() 命令:从 Cypress 9.6.0 起,可通过 cy.origin() 在不同域上执行操作,Cypress 会为新源创建 iframe 并通过 postMessage 通信。这是官方推荐的跨域测试方案。// 方式一:禁用 Chrome Web 安全// cypress.config.jsmodule.exports = { e2e: { chromeWebSecurity: false }};// 方式二:cy.origin()cy.origin("https://example.com", () => { cy.visit("/login"); cy.get("input[name=email]").type("test@example.com"); cy.get("button[type=submit]").click();});追问:chromeWebSecurity: false 有什么局限?仅对 Chromium 内核浏览器有效,Firefox 等浏览器不支持此选项不会绕过 cy.visit() 的超域限制,即不能在同一个测试中 cy.visit() 不同超域的 URL生产环境不存在此开关,测试可能掩盖真实的跨域问题追问:cy.origin() 的注意事项?回调函数内无法直接引用外部作用域的变量,需通过第二个参数传入:const username = "test@example.com";cy.origin("https://example.com", { args: { username } }, ({ username }) => { cy.get("input[name=email]").type(username);});回调内不能使用 cy.session()、自定义命令等部分 API每次调用 cy.origin() 会创建新的 iframe 上下文,有性能开销追问:还有其他方案吗?可通过服务器端反向代理(如 nginx)将不同域的 API 映射到同源路径下,从根本上消除跨域。这种方式不依赖 Cypress 配置,但需要额外的基础设施支持。# nginx 反向代理示例location /external-api/ { proxy_pass https://api.example.com/;}
服务端阅读 05月27日 23:17

Gradle 如何实现多项目构建?项目间依赖怎么配置?

Gradle 通过 settings.gradle 声明子项目,通过 project() 依赖建立项目间引用,配合 allprojects/subprojects 共享配置,实现多项目构建。核心机制多项目构建由一个根项目和若干子项目组成。settings.gradle 定义了哪些子项目参与构建,Gradle 据此建立项目间的依赖图并决定构建顺序。// settings.gradlerootProject.name = 'my-project'include 'app', 'library', 'common'// 嵌套项目include ':data:repository'project(':data:repository').projectDir = new File(rootDir, 'modules/data/repository')项目间依赖配置子项目之间通过 project() 函数声明依赖,Gradle 自动保证被依赖项目先构建:// app/build.gradledependencies { implementation project(':library') implementation project(':common') // 指定配置 implementation project(path: ':library', configuration: 'runtimeClasspath')}// library/build.gradledependencies { api project(':common') // api 会传递依赖给上层}implementation 与 api 的区别在多项目中尤为关键:api 暴露依赖给消费方,implementation 则不传递。共享配置的三种方式1. allprojects / subprojects 注入// 根 build.gradleallprojects { group = 'com.example' version = '1.0.0' repositories { mavenCentral() }}subprojects { apply plugin: 'java' java { sourceCompatibility = JavaVersion.VERSION_17 }}2. 约定插件(Convention Plugin)在 buildSrc 或独立插件项目中定义,比 subprojects 更灵活、可测试:// buildSrc/src/main/groovy/my-java-convention.gradleplugins { id 'java-library'}java { sourceCompatibility = JavaVersion.VERSION_17}// 子项目使用// library/build.gradleplugins { id 'my-java-convention' }3. 版本目录(Version Catalog)# gradle/libs.versions.toml[versions]spring-boot = "3.0.0"[libraries]spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }// 子项目中使用dependencies { implementation libs.spring.boot.web}构建执行./gradlew projects # 查看项目结构./gradlew :app:build # 构建指定项目(自动构建其依赖项)./gradlew build --parallel # 并行构建并行构建需要在 gradle.properties 中启用:org.gradle.parallel=trueorg.gradle.caching=trueorg.gradle.configureondemand=true循环依赖问题项目 A 依赖 B,B 又依赖 A,Gradle 构建时会报错。解决方式是抽取公共部分到第三个模块,或者用 compileOnly/testImplementation 打断传递链。可通过 ./gradlew :app:dependencies --configuration runtimeClasspath 排查依赖链路。追问allprojects 和 subprojects 有什么区别?——allprojects 包含根项目,subprojects 只作用于子项目。约定插件相比 subprojects 闭包有什么优势?——可复用、可测试、支持按需引入,避免全局污染。如何让子项目独立构建而不触发其他项目?——configureondemand 配合按需配置,Gradle 6+ 默认部分启用。
服务端阅读 05月27日 23:16

Gradle 依赖管理有哪些配置类型?如何解决冲突?

Gradle 通过 configurations(依赖配置)和 repositories(仓库)两大机制管理依赖。configurations 定义依赖的可见范围和生命周期,repositories 定义依赖的获取来源。核心依赖配置类型| 配置 | 编译时 | 运行时 | 传递给消费者 | 典型场景 ||------|--------|--------|--------------|----------|| implementation | 可见 | 可见 | 不可见 | 默认选择,大多数依赖用这个 || api | 可见 | 可见 | 可见 | 库模块需要暴露给下游的 API 依赖 || compileOnly | 可见 | 不可见 | 不可见 | Lombok 等仅编译期需要的依赖 || runtimeOnly | 不可见 | 可见 | 不可见 | JDBC 驱动等仅运行时需要的依赖 || annotationProcessor | 可见 | 不可见 | 不可见 | 注解处理器 |dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' api 'org.apache.commons:commons-math3:3.6.1' // 消费者也能访问 compileOnly 'org.projectlombok:lombok:1.18.24' // 编译后丢弃 runtimeOnly 'mysql:mysql-connector-java:8.0.28' // 运行时才加载 annotationProcessor 'org.projectlombok:lombok:1.18.24'}implementation 与 api 的关键区别: implementation 的依赖不会出现在消费者的编译类路径中,改动后只重新编译当前模块;api 会传递,改动后消费者也要重新编译。优先用 implementation 可以显著缩短构建时间。版本管理推荐使用版本目录(Version Catalog),在 gradle/libs.versions.toml 中集中声明:[versions]spring-boot = "3.0.0"[libraries]spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }dependencies { implementation libs.spring.boot.web}多模块项目也可用 BOM 统一版本:dependencies { implementation platform('org.springframework.boot:spring-boot-dependencies:3.0.0') implementation 'org.springframework.boot:spring-boot-starter-web' // 版本由 BOM 控制}依赖冲突与排除Gradle 默认取最高版本。当冲突不可自动解决时,可强制版本或排除:// 排除特定传递依赖implementation('org.springframework.boot:spring-boot-starter-web:3.0.0') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'}// 强制版本configurations.all { resolutionStrategy { force 'org.apache.commons:commons-lang3:3.12.0' }}查看依赖树排查冲突:./gradlew dependencies --configuration implementation追问Q: 为什么 implementation 改了不会触发下游模块重编译?A: implementation 的依赖被隔离在当前模块的编译类路径内,消费者编译时看不到这些依赖,因此改动不影响消费者的编译产物。这是 Gradle 3.4 引入 implementation 替代 compile 的核心动机。Q: 什么时候必须用 api 而不能用 implementation?A: 当模块的公开 API 方法签名中用到了某个依赖的类型时(如返回值或参数类型来自该依赖),必须用 api,否则消费者编译时会找不到类。Q: 动态版本(如 3.+)有什么问题?A: 构建不可复现——同一份代码不同时间构建可能拉到不同版本,导致线上行为不一致。生产环境必须锁定版本。
服务端阅读 05月27日 23:16

Gradle 的生命周期包括哪些阶段?

三个阶段:初始化 → 配置 → 执行Gradle 构建生命周期分为 初始化(Initialization)、配置(Configuration)、执行(Execution) 三个阶段,依次完成项目确定、任务图构建和任务执行。初始化阶段读取 settings.gradle,确定参与构建的项目并为每个项目创建 Project 实例:// settings.gradlerootProject.name = 'my-app'include 'app', 'core', 'utils'钩子:gradle.projectsLoaded配置阶段执行所有项目的 build.gradle,配置插件、依赖和任务,建立任务依赖图。无论最终执行哪个任务,所有项目的构建脚本都会被执行——这是性能问题的常见来源。// 延迟注册任务,避免配置阶段不必要的开销tasks.register('myTask') { onlyIf { project.hasProperty('enableMyTask') }}钩子:gradle.beforeProject / gradle.afterProject执行阶段按依赖图顺序执行目标任务及其依赖。支持增量构建(仅处理变更文件)和并行执行(--parallel)。钩子:gradle.taskGraph.whenReady / gradle.taskGraph.beforeTask / gradle.taskGraph.afterTask常见追问Q: 为什么配置阶段会成为性能瓶颈?每个项目的 build.gradle 都会执行,即使只跑一个任务。大量 I/O 或复杂逻辑放在配置阶段会拖慢整个构建。应使用 tasks.register() 延迟创建、启用 --configuration-cache 缓存配置结果。Q: afterEvaluate 的作用?在项目配置完成后回调,常用于在插件配置完成后再添加依赖或修改属性,确保顺序正确。Q: 增量构建的原理?Gradle 为每个 Task 维护输入输出的哈希,若输入输出均未变化则跳过执行(标记 UP-TO-DATE),而非真正重新运行。