那天下午,当我正悠闲地喝着咖啡调试接口时,Postman突然弹出一个熟悉的错误——"No message available"。这个错误信息在线上环境已经困扰我们团队好几周了,没想到在本地也能复现。作为一名有经验的Spring Cloud开发者,我决定深入挖掘这个看似简单却隐藏着复杂机制的问题。
在本地环境中,当我通过Postman请求http://localhost:8099/api/cbs/area/getProvinceList时,服务返回了以下错误响应:
json复制{
"code": "9000",
"message": "服务器开小差了"
}
查看应用日志,发现底层抛出的实际错误信息是:
code复制No message available
有趣的是,当我移除URL中的/api前缀,直接请求http://localhost:8099/cbs/area/getProvinceList时,接口却能正常响应。这种差异立刻引起了我的警觉——问题很可能出在请求路径的处理上。
关键观察点:
/api前缀的请求失败既然问题与路径前缀有关,我自然将怀疑的目光投向了Spring Cloud Gateway的配置。在我们的架构中,所有外部请求都经过Gateway转发到具体的微服务。
检查Apollo配置中心,发现Gateway的路由配置中存在这样的条目:
yaml复制spring:
cloud:
gateway:
routes:
- id: aba-cbs-provider
uri: lb://aba-cbs-provider
predicates:
- Path=/api/cbs/**
filters:
- StripPrefix=1
这个配置看起来很简单:匹配以/api/cbs开头的请求,去掉第一个路径段(即/api),然后将请求转发到cbs服务。理论上,/api/cbs/area/getProvinceList应该变成/cbs/area/getProvinceList。
但为什么实际效果与预期不符?我开始怀疑StripPrefix过滤器的工作机制是否如我所想。
为了验证我的猜想,我决定深入研究StripPrefix过滤器的工作原理。查阅Spring Cloud Gateway官方文档后,我整理出以下关键点:
| 配置示例 | 原始请求路径 | 处理后路径 | 说明 |
|---|---|---|---|
| StripPrefix=1 | /api/cbs/area/getProvinceList | /cbs/area/getProvinceList | 移除第一个路径段 |
| StripPrefix=2 | /api/cbs/area/getProvinceList | /area/getProvinceList | 移除前两个路径段 |
| 无StripPrefix | /api/cbs/area/getProvinceList | /api/cbs/area/getProvinceList | 保持原样 |
看起来配置是正确的,但问题依然存在。于是,我决定通过调试来观察实际的请求转发过程。
在Gateway服务中设置断点,跟踪请求处理流程,我发现:
/api/cbs/area/getProvinceList被正确匹配到路由规则StripPrefix=1确实移除了/api前缀/cbs/area/getProvinceList但是,为什么下游服务还是返回了路径匹配错误?这引导我将注意力转向了下游服务本身的配置。
检查cbs服务的Controller代码,我发现:
java复制@RestController
@RequestMapping("/area")
public class AreaController {
@GetMapping("/getProvinceList")
public List<Province> getProvinceList() {
// 业务逻辑
}
}
这里的关键发现是:Controller的@RequestMapping只映射了/area路径,而Gateway转发后的路径是/cbs/area/getProvinceList。这意味着:
/cbs/area/getProvinceList/area/getProvinceList这种不匹配解释了为什么请求会失败。那么,为什么移除/api前缀后直接请求/cbs/area/getProvinceList又能成功呢?
原来,我们的服务部署采用了两种方式:
/api/cbs/.../cbs/...而Controller实际上是为直接访问设计的,没有考虑Gateway转发后的路径变化。
基于以上分析,我提出了几种解决方案:
方案一:调整StripPrefix值
yaml复制filters:
- StripPrefix=2 # 移除/api/cbs
这样会将/api/cbs/area/getProvinceList变为/area/getProvinceList,匹配Controller的映射。
方案二:统一Controller路径
java复制@RestController
@RequestMapping("/cbs/area")
public class AreaController {
// ...
}
方案三:使用RewritePath过滤器
yaml复制filters:
- RewritePath=/api/cbs/(?<segment>.*), /$\{segment}
经过团队讨论,我们最终选择了方案一,因为它:
Gateway配置自查清单:
Path谓词与StripPrefix值的匹配关系在解决这个问题的过程中,我发现Spring Cloud Gateway的路由优先级规则也是容易导致配置错误的一个点。Gateway按照以下顺序评估路由规则:
例如:
yaml复制routes:
- id: specific-route
predicates:
- Path=/api/user/detail
# ...
- id: general-route
predicates:
- Path=/api/user/**
# ...
在这个例子中,/api/user/detail会匹配第一个路由,即使它也符合第二个路由的模式。这种特性使得我们可以实现"从具体到通用"的路由策略。
这次排查经历让我深刻认识到,在微服务架构中,Gateway配置与下游服务的路径映射必须保持严格一致。以下是我总结的几条经验:
在代码层面,我们可以采用更防御性的编程方式:
java复制@RestController
@RequestMapping("${service.api.prefix:/}area")
public class AreaController {
// ...
}
这样可以通过配置灵活控制前缀,适应不同的访问方式。
经过这次排查,我们不仅解决了"No message available"的问题,还建立了一套更健壮的Gateway配置审查机制。现在,每当看到这个错误信息,我都会条件反射般地检查路径映射关系——这也许就是所谓的"成长"吧。