目录
- 问题背景
- Gateway集成若依鉴权需求
- HandlerMethod空指针报错
- 为什么路由转发没有HandlerMethod
- HandlerMethod原理
- HandlerMethod是什么
- Spring如何包装Controller方法
- HandlerMethod包含的信息
- 路由转发机制
- 本地方法处理 vs 路由转发
- Gateway转发流程
- 为什么转发请求没有HandlerMethod
- 问题根因定位
- 注册路由工具类问题
- 匹配速度过慢
- 规则简陋导致的bug
- 解决方案
- 简化路由匹配规则
- 重新注册路由
- 验证HandlerMethod获取
- 最佳实践
- Gateway权限控制推荐方案
- 注解vs配置的权衡
- WebFlux特殊注意事项
- 总结
- 问题本质
- 根本原因
- 解决方案
- 经验教训
- 参考资料
- 标签
一、问题背景
1.1 Gateway集成若依鉴权需求
在微服务架构体系里,我们选用Spring Cloud Gateway作为API网关,旨在集成若依框架的统一鉴权功能。具体需求如下:
- 网关层面的统一权限验证:确保在网关处对所有请求进行统一的权限校验。
- 支持基于注解的权限控制(
@RemotePreAuthorize):借助注解来灵活定义不同接口的权限控制逻辑。 - 通过RemoteAuthWebFilter拦截请求进行权限验证:利用该过滤器对进入的请求实施权限验证操作。
- 调用若依鉴权中心验证用户权限:与若依鉴权中心交互,确认用户是否具备相应的访问权限。
1.2 HandlerMethod空指针报错
在集成过程中,出现了部分请求正常,而部分请求报空指针异常的情况:
java.lang.NullPointerException:handlerMethod isnullatRemoteAuthWebFilter.getHandlerMethod(ServerWebExchange)atRemoteAuthWebFilter.validateAuthorization(ServerWebExchange)atRemoteAuthWebFilter.filter(ServerWebExchange,GatewayFilterChain)问题特点:
- 本地接口请求(如
/health/services)正常:对于网关内部的本地接口请求能够正常处理。 - 路由转发请求(如
/cm/contracts)报错:经过网关路由转发到后端服务的请求则出现空指针异常。
1.3 为什么路由转发没有HandlerMethod
经过初步分析,发现问题的关键在于:
| 请求类型 | 示例路径 | HandlerMethod | 处理位置 |
|---|---|---|---|
| 本地方法 | /health/services | ✅ 有 | Gateway Controller |
| 路由转发 | /cm/contracts | ❌ 无 | 后端服务 |
核心矛盾:
- 本地方法:Spring能够找到本地的Controller方法,并创建HandlerMethod对其进行包装。
- 路由转发:Gateway仅作为代理转发请求,并不执行本地方法,所以不存在HandlerMethod。
比如,想象你要去一个小区找朋友(请求到达),小区门口的保安(Gateway)有两种情况。如果朋友就住在小区门口的保安室旁边(本地方法),保安很容易就找到你朋友(创建HandlerMethod)。但如果朋友住在小区里面的某栋楼(后端服务),保安只是给你指了路,让你自己过去(路由转发),保安这里并没有你朋友的具体信息(没有HandlerMethod)。
二、HandlerMethod原理
2.1 HandlerMethod是什么
HandlerMethod是Spring框架中用于封装Controller处理方法的类。它如同连接HTTP请求与业务逻辑的一座桥梁,将外部请求与内部具体的业务处理函数关联起来。
2.2 Spring如何包装Controller方法
当一个HTTP请求抵达Spring MVC/WebFlux应用时,其处理流程如下:
HTTP请求 → DispatcherHandler → HandlerMapping → 找到处理方法 → 创建HandlerMethod → 执行方法流程详解:
- 请求到达:客户端发送HTTP请求,如同快递包裹被送到了一个处理中心(应用)。
- 路由匹配:HandlerMapping根据请求的URL,就像根据快递的收件地址,找到对应的处理方法。
- 方法包装:Spring创建HandlerMethod对象,这个对象就像一个装满了方法详细信息的包裹,包含方法的各种属性和参数等完整信息。
- 权限检查:从HandlerMethod中获取注解,比如检查包裹上的特殊标记,进行权限验证。
- 方法执行:调用实际的业务方法,就像按照包裹里的说明进行具体的操作。
2.3 HandlerMethod包含的信息
HandlerMethod是一个信息丰富的载体,包含:
| 信息类型 | 说明 | 用途 |
|---|---|---|
| Method 对象 | Java反射方法 | 执行业务逻辑,好比是具体做事的工具 |
| Bean 实例 | Controller对象 | 访问实例变量,如同进入一个房间获取里面的物品 |
| 注解信息 | 方法上的所有注解 | 权限验证、AOP等,类似给做事的过程加上各种规则和条件 |
| 参数信息 | 方法参数类型和注解 | 参数绑定、验证,确保输入的信息是符合要求的 |
为什么HandlerMethod对鉴权重要?
因为鉴权注解(如@RemotePreAuthorize)是写在Controller方法上的,例如:
@RestController@RequestMapping("/health")publicclassHealthStatusController{@RemotePreAuthorize("@ss.hasRole('admin')")// ← 鉴权注解@GetMapping("/services")publicResponseEntity<Map<String,Object>>getAllServiceHealth(){// 业务逻辑}}鉴权流程:
三、路由转发机制
3.1 本地方法处理 vs 路由转发
Spring Cloud Gateway存在两种请求处理模式:
本地方法处理
// Gateway 中的本地 Controller@RestController@RequestMapping("/health")publicclassHealthStatusController{@GetMapping("/services")publicResponseEntity<?>getAllServiceHealth(){// 返回各服务健康状态}}- 请求路径:
/health/services - HandlerMethod:✅ 存在
- 鉴权方式:RemoteAuthWebFilter获取HandlerMethod → 读取注解 → 验证权限
路由转发
# application.yml 中的路由配置spring:cloud:gateway:routes:-id:contract-managementuri:lb://contract-managementpredicates:-Path=/cm/**filters:-RewritePath=/cm/(?<path>.*),/${path}- 请求路径:
/cm/contracts - HandlerMethod:❌ 不存在
- 处理方式:Gateway修改请求URI → 转发到后端服务
3.2 Gateway转发流程
路由转发的完整流程:
3.3 为什么转发请求没有HandlerMethod
这是问题的核心所在:
本质区别:
| 维度 | 本地方法 | 路由转发 |
|---|---|---|
| 执行位置 | Gateway内部 | 后端服务 |
| Controller | Gateway的Controller | 后端服务的Controller |
| HandlerMethod | Gateway创建 | 后端服务创建 |
| 鉴权时机 | 在Gateway内 | 由后端服务处理 |
关键理解:
Gateway在路由转发场景下,就像是一个快递中转站,不是请求的最终处理者。它只是接收请求(收到快递),修改URI(重新写快递地址),转发给后端服务(把快递送到下一个站点),后端服务处理请求并返回响应(最终站点处理快递并给出反馈)。因此,Gateway内部没有对应的Controller方法,也就没有HandlerMethod。
四、问题根因定位
4.1 注册路由工具类问题
我们项目中有一个路由注册工具类,用于动态管理路由规则:
// 问题代码(简化示例)@ComponentpublicclassRouteRegistry{publicbooleanisLocalRoute(Stringpath){// 简陋的路由匹配逻辑returnpath.startsWith("/health")||path.startsWith("/admin");}publicHandlerMethodgetHandlerMethod(Stringpath){// 只有本地路由才查找 HandlerMethodif(!isLocalRoute(path)){returnnull;// ← 问题所在!}// 查找逻辑...}}4.2 匹配速度过慢
这个工具类的问题之一是匹配效率低:
// 问题:逐个遍历所有路由规则publicbooleanisLocalRoute(Stringpath){for(RouteRulerule:routeRules){// O(n) 复杂度if(path.matches(rule.getPattern())){returntrue;}}returnfalse;}性能问题:
- 每次请求都要遍历所有规则,就像每次找东西都要把所有东西翻一遍。
- 正则匹配开销大,增加了处理时间。
- 路由规则越多,性能越差,东西越多找起来越慢。
4.3 规则简陋导致的bug
更严重的问题是规则判断过于简单:
// 只检查固定前缀publicbooleanisLocalRoute(Stringpath){returnpath.startsWith("/health")||path.startsWith("/admin");}问题场景:
| 请求路径 | isLocalRoute() | 实际应该是 | 结果 |
|---|---|---|---|
/health/services | true | 本地方法 | ✅ 正确 |
/admin/cache | true | 本地方法 | ✅ 正确 |
/csr/validate | false | 路由转发 | ✅ 正确 |
/cm/contracts | false | 路由转发 | ✅ 正确 |
/metrics | false | 本地方法! | ❌ 错误 |
Debug现场验证:
// RemoteAuthWebFilter.java@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){HandlerMethodhandlerMethod=getHandlerMethod(exchange);// Debug发现:asserthandlerMethod==null;// ← 空指针的根源!// 后续代码尝试访问 handlerMethod 的方法if(handlerMethod.hasAnnotation()){// ← NullPointerException!// ...}}五、解决方案
5.1 简化路由匹配规则
核心思路:移除自定义路由工具类,使用Gateway原生能力。
方案一:基于路径前缀区分(推荐)
# application.ymlspring:cloud:gateway:routes:# 本地接口使用特定前缀-id:local-healthuri:lb://contract-gateway# 转发给自己predicates:-Path=/gateway/health/**filters:-StripPrefix=1# 后端服务路由-id:contract-managementuri:lb://contract-managementpredicates:-Path=/cm/**权限处理策略:
/gateway/**开头的请求 → Gateway本地处理,使用HandlerMethod鉴权。- 其他路径 → 转发给后端服务,由后端服务自行鉴权。
方案二:统一网关鉴权(适用于严格权限控制)
// RemoteAuthWebFilter 修改版@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpath=exchange.getRequest().getPath().value();// 判断是否为路由转发请求if(isRouteForwarding(path)){// 不尝试获取 HandlerMethod,直接进行统一鉴权returnvalidateRemoteAuth(exchange,chain);}else{// 本地方法,获取 HandlerMethod 进行注解鉴权HandlerMethodhandlerMethod=getHandlerMethod(exchange);returnvalidateAnnotationAuth(exchange,chain,handlerMethod);}}5.2 重新注册路由
移除复杂的路由工具类后,使用Gateway原生配置:
# application.yml - 清晰的路由配置spring:cloud:gateway:routes:# === Gateway 本地接口 ===-id:health-checkuri:lb://contract-gatewaypredicates:-Path=/health/**filters:-StripPrefix=0-id:admin-apiuri:lb://contract-gatewaypredicates:-Path=/admin/**filters:-StripPrefix=0# === 后端服务路由 ===-id:contract-managementuri:lb://contract-managementpredicates:-Path=/cm/**filters:-RewritePath=/cm/(?<path>.*),/${path}-id:contract-security-ruoyiuri:lb://contract-security-ruoyipredicates:-Path=/csr/**filters:-RewritePath=/csr/(?<path>.*),/${path}-id:contract-review-engineuri:lb://contract-review-enginepredicates:-Path=/cre/**filters:-RewritePath=/cre/(?<path>.*),/${path}配置说明:
| 路由ID | 路径规则 | 目标服务 | 鉴权方式 |
|---|---|---|---|
| health-check | /health/** | Gateway本地 | HandlerMethod + 注解 |
| admin-api | /admin/** | Gateway本地 | HandlerMethod + 注解 |
| contract-management | /cm/** | 后端服务 | 后端服务自行鉴权 |
5.3 验证HandlerMethod获取
修复后的验证测试:
// 测试用例@TestpublicvoidtestHandlerMethodRetrieval(){// 本地方法请求HandlerMethodhm1=getHandlerMethod("/health/services");assertNotNull(hm1);assertTrue(hm1.hasMethodAnnotation(Anonymous.class));// 路由转发请求 - 不再期望获取 HandlerMethodHandlerMethodhm2=getHandlerMethod("/cm/contracts");assertNull(hm2);// ← 预期行为,不再是 bug}六、最佳实践
6.1 Gateway权限控制推荐方案
根据实践经验,推荐以下方案:
| 场景 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| 网关本地接口 | @RemotePreAuthorize + HandlerMethod | 代码即配置,类型安全 | 只适合本地方法 |
| 路由转发 | 后端服务自行鉴权 | 职责分离,灵活 | 每个服务都要实现 |
| 统一鉴权 | RemoteAuthWebFilter统一拦截 | 集中管理,安全 | 无法细粒度控制 |
推荐架构: