目录
- 问题背景
- HandlerMethod原理
- 路由转发机制
- [本地方法处理 vs 路由转发](#本地方法处理 vs 路由转发)
- Gateway转发流程
- 为什么转发请求没有HandlerMethod
- 问题根因定位
- 解决方案
- 最佳实践
- 总结
- 参考资料
- 标签
一、问题背景
1.1 Gateway集成若依鉴权需求
在微服务架构体系里,我们选用Spring Cloud Gateway作为API网关,旨在集成若依框架的统一鉴权功能。具体需求如下:
- 网关层面的统一权限验证:确保在网关处对所有请求进行统一的权限校验。
- 支持基于注解的权限控制(
@RemotePreAuthorize):借助注解来灵活定义不同接口的权限控制逻辑。 - 通过RemoteAuthWebFilter拦截请求进行权限验证:利用该过滤器对进入的请求实施权限验证操作。
- 调用若依鉴权中心验证用户权限:与若依鉴权中心交互,确认用户是否具备相应的访问权限。
1.2 HandlerMethod空指针报错
在集成过程中,出现了部分请求正常,而部分请求报空指针异常的情况:
java
java.lang.NullPointerException: handlerMethod is null
at RemoteAuthWebFilter.getHandlerMethod(ServerWebExchange)
at RemoteAuthWebFilter.validateAuthorization(ServerWebExchange)
at RemoteAuthWebFilter.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方法上的,例如:
java
@RestController
@RequestMapping("/health")
public class HealthStatusController {
@RemotePreAuthorize("@ss.hasRole('admin')") // ← 鉴权注解
@GetMapping("/services")
public ResponseEntity<Map<String, Object>> getAllServiceHealth() {
// 业务逻辑
}
}
鉴权流程:
通过
失败
HTTP请求
创建HandlerMethod
提取RemotePreAuthorize注解
解析权限表达式
调用若依鉴权中心
权限验证
执行Controller方法
返回403 Forbidden
三、路由转发机制
3.1 本地方法处理 vs 路由转发
Spring Cloud Gateway存在两种请求处理模式:
本地方法处理
java
// Gateway 中的本地 Controller
@RestController
@RequestMapping("/health")
public class HealthStatusController {
@GetMapping("/services")
public ResponseEntity<?> getAllServiceHealth() {
// 返回各服务健康状态
}
}
- 请求路径 :
/health/services - HandlerMethod:✅ 存在
- 鉴权方式:RemoteAuthWebFilter获取HandlerMethod → 读取注解 → 验证权限
路由转发
yaml
# application.yml 中的路由配置
spring:
cloud:
gateway:
routes:
- id: contract-management
uri: lb://contract-management
predicates:
- Path=/cm/**
filters:
- RewritePath=/cm/(?<path>.*), /${path}
- 请求路径 :
/cm/contracts - HandlerMethod:❌ 不存在
- 处理方式:Gateway修改请求URI → 转发到后端服务
3.2 Gateway转发流程
路由转发的完整流程:
否
请求 /cm/contracts
Gateway路由匹配
本地方法?
查找路由规则
修改URI为/contracts
转发到contract-management服务
后端服务处理
3.3 为什么转发请求没有HandlerMethod
这是问题的核心所在:
本质区别:
| 维度 | 本地方法 | 路由转发 |
|---|---|---|
| 执行位置 | Gateway内部 | 后端服务 |
| Controller | Gateway的Controller | 后端服务的Controller |
| HandlerMethod | Gateway创建 | 后端服务创建 |
| 鉴权时机 | 在Gateway内 | 由后端服务处理 |
关键理解 :
Gateway在路由转发场景下,就像是一个快递中转站,不是请求的最终处理者。它只是接收请求(收到快递),修改URI(重新写快递地址),转发给后端服务(把快递送到下一个站点),后端服务处理请求并返回响应(最终站点处理快递并给出反馈)。因此,Gateway内部没有对应的Controller方法,也就没有HandlerMethod。
四、问题根因定位
4.1 注册路由工具类问题
我们项目中有一个路由注册工具类,用于动态管理路由规则:
java
// 问题代码(简化示例)
@Component
public class RouteRegistry {
public boolean isLocalRoute(String path) {
// 简陋的路由匹配逻辑
return path.startsWith("/health") || path.startsWith("/admin");
}
public HandlerMethod getHandlerMethod(String path) {
// 只有本地路由才查找 HandlerMethod
if (!isLocalRoute(path)) {
return null; // ← 问题所在!
}
// 查找逻辑...
}
}
4.2 匹配速度过慢
这个工具类的问题之一是匹配效率低:
java
// 问题:逐个遍历所有路由规则
public boolean isLocalRoute(String path) {
for (RouteRule rule : routeRules) { // O(n) 复杂度
if (path.matches(rule.getPattern())) {
return true;
}
}
return false;
}
性能问题:
- 每次请求都要遍历所有规则,就像每次找东西都要把所有东西翻一遍。
- 正则匹配开销大,增加了处理时间。
- 路由规则越多,性能越差,东西越多找起来越慢。
4.3 规则简陋导致的bug
更严重的问题是规则判断过于简单:
java
// 只检查固定前缀
public boolean isLocalRoute(String path) {
return path.startsWith("/health") || path.startsWith("/admin");
}
问题场景:
| 请求路径 | isLocalRoute() | 实际应该是 | 结果 |
|---|---|---|---|
/health/services |
true |
本地方法 | ✅ 正确 |
/admin/cache |
true |
本地方法 | ✅ 正确 |
/csr/validate |
false |
路由转发 | ✅ 正确 |
/cm/contracts |
false |
路由转发 | ✅ 正确 |
/metrics |
false |
本地方法! | ❌ 错误 |
Debug现场验证:
java
// RemoteAuthWebFilter.java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HandlerMethod handlerMethod = getHandlerMethod(exchange);
// Debug发现:
assert handlerMethod == null; // ← 空指针的根源!
// 后续代码尝试访问 handlerMethod 的方法
if (handlerMethod.hasAnnotation()) { // ← NullPointerException!
// ...
}
}
五、解决方案
5.1 简化路由匹配规则
核心思路:移除自定义路由工具类,使用Gateway原生能力。
方案一:基于路径前缀区分(推荐)
yaml
# application.yml
spring:
cloud:
gateway:
routes:
# 本地接口使用特定前缀
- id: local-health
uri: lb://contract-gateway # 转发给自己
predicates:
- Path=/gateway/health/**
filters:
- StripPrefix=1
# 后端服务路由
- id: contract-management
uri: lb://contract-management
predicates:
- Path=/cm/**
权限处理策略:
/gateway/**开头的请求 → Gateway本地处理,使用HandlerMethod鉴权。- 其他路径 → 转发给后端服务,由后端服务自行鉴权。
方案二:统一网关鉴权(适用于严格权限控制)
java
// RemoteAuthWebFilter 修改版
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 判断是否为路由转发请求
if (isRouteForwarding(path)) {
// 不尝试获取 HandlerMethod,直接进行统一鉴权
return validateRemoteAuth(exchange, chain);
} else {
// 本地方法,获取 HandlerMethod 进行注解鉴权
HandlerMethod handlerMethod = getHandlerMethod(exchange);
return validateAnnotationAuth(exchange, chain, handlerMethod);
}
}
5.2 重新注册路由
移除复杂的路由工具类后,使用Gateway原生配置:
yaml
# application.yml - 清晰的路由配置
spring:
cloud:
gateway:
routes:
# === Gateway 本地接口 ===
- id: health-check
uri: lb://contract-gateway
predicates:
- Path=/health/**
filters:
- StripPrefix=0
- id: admin-api
uri: lb://contract-gateway
predicates:
- Path=/admin/**
filters:
- StripPrefix=0
# === 后端服务路由 ===
- id: contract-management
uri: lb://contract-management
predicates:
- Path=/cm/**
filters:
- RewritePath=/cm/(?<path>.*), /${path}
- id: contract-security-ruoyi
uri: lb://contract-security-ruoyi
predicates:
- Path=/csr/**
filters:
- RewritePath=/csr/(?<path>.*), /${path}
- id: contract-review-engine
uri: lb://contract-review-engine
predicates:
- Path=/cre/**
filters:
- RewritePath=/cre/(?<path>.*), /${path}
配置说明:
| 路由ID | 路径规则 | 目标服务 | 鉴权方式 |
|---|---|---|---|
| health-check | /health/** |
Gateway本地 | HandlerMethod + 注解 |
| admin-api | /admin/** |
Gateway本地 | HandlerMethod + 注解 |
| contract-management | /cm/** |
后端服务 | 后端服务自行鉴权 |
5.3 验证HandlerMethod获取
修复后的验证测试:
java
// 测试用例
@Test
public void testHandlerMethodRetrieval() {
// 本地方法请求
HandlerMethod hm1 = getHandlerMethod("/health/services");
assertNotNull(hm1);
assertTrue(hm1.hasMethodAnnotation(Anonymous.class));
// 路由转发请求 - 不再期望获取 HandlerMethod
HandlerMethod hm2 = getHandlerMethod("/cm/contracts");
assertNull(hm2); // ← 预期行为,不再是 bug
}
六、最佳实践
6.1 Gateway权限控制推荐方案
根据实践经验,推荐以下方案:
| 场景 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| 网关本地接口 | @RemotePreAuthorize + HandlerMethod | 代码即配置,类型安全 | 只适合本地方法 |
| 路由转发 | 后端服务自行鉴权 | 职责分离,灵活 | 每个服务都要实现 |
| 统一鉴权 | RemoteAuthWebFilter统一拦截 | 集中管理,安全 | 无法细粒度控制 |
推荐架构:
本地接口
路由转发
客户端请求
Gateway
请求类型
HandlerMethod鉴权
转发到后端服务