1. 微服务的公共API模块
微服务之间调用进程会出现DTO实体类的重复定义。比如服务A的接口返回User实体,服务B接收的时候,也需要定义一个同样的User实体。
在引入了Feign后,就有了一个避免项目间重复定义实体类的简单方案:我们可以在服务A开发的时候专门抽出来一个API模块。
这个API模块可以包含接口方法定义,URI以及和对外实体类定义(DTO),可以认为是A和B之间互通的约定。
一个最简单的API模块代码如下:
1 |
|
服务A的Controller负责对接口定义进行实现:
1 |
|
服务A项目将API模块发布到Maven私服上。服务B项目只需要对API模块添加依赖:
1 | <dependency> |
并且扩展一下该接口并添加@FeignClient
注解:
1 |
|
就可以很轻松地像调用本地方法一样调用A应用的接口了。
1 |
|
2. Ambiguous mapping报错
如果你像我上面描述的那样实现,就会在消费者服务B启动的时候遇到如下的报错信息:
1 | Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'com.galaxy.demo.feign.consumer.spi.DemoSpiService' method |
报错信息很直白:同一个URI被重复映射了两次。一次是在DemoConsumerController,一次是在DemoSpiService。
But Why? DemoSpiService里只是一个FeignClient,不是RestController啊?
3. @RestController,@Controller,@RequestMapping原理重温
我们通过这个问题,正好来重温一下@RestController,@Controller和@RequestMapping几个Spring中的经典概念。
3.1 @RestController
我们先来看一下@RestController的原代码:
1 |
|
可以看到@RestController=@Controller+@ResponseBody
上图是一个Spring MVC从接收请求到返回响应的完整流程。我理解对于SpringBoot的RestController来说,在第四步没有返回ModelAndView,而是直接返回了Json,并通过@ResponseBody将Json直接写到了响应Body,略过了第5步和第6步。
3.2 @Controller和@RequestMapping
如果只从@Controller的源代码来看,@Controller只是@Component的一个别名。
1 |
|
但注解怎么用不是看定义的。从Spring的AbstractHandlerMethodMapping.java的源代码,我们可以看到Spring会根据一个名为isHandler方法的判断结果,对Handler处理器里的方法进行扫描,获得URL映射。
1 | if (beanType != null && isHandler(beanType)) { |
而isHandler的逻辑很简单,就是看Bean上是否有@Controller注解或@RequestMapping注解。参见源代码:
1 |
|
4. Ambiguous mapping报错原因总结和解决方案
归根到底,Ambiguous mapping报错原因在于上面的那个逻辑中使用的是“或”(||),而不是“和”(&&)。
由于我们的DemoSpiService扩展了DemoApiService,而DemoApiService的接口定义上有@RequestMapping注解,于是DemoSpiService也被Spring MVC扫描Handler了。而偏生对于DemoSpiService和DemoConsumerController的URL路径都是“/demo”,于是就产生了冲突。
知道了原因后,解决方案也就很简单了:修改一下DemoConsumerController的@RequestMapping的URL,例如改为@RequestMapping("/consumer/demo")
,就可以成功启动了。
你可能会担心@FeignClient+API模块是否会暴露不该暴露的接口?直接访问的话会返回404:
1 | { |
也很容易理解:请求在找RequestMapping对应的View:”/demo/hello”。但View不存在,就只能返回404了。只有在DemoSpiService上主动添加@ResponseBody注解,才能对外暴露。
5. 参考资料
这篇是主要参考资料。作者认为这是Spring MVC的锅。我理解指的是“或”的那个逻辑。但我觉得当初Spring这么写肯定是有原因的。。。虽然我没找到相关文章。
FeignClient 出现 Ambiguous mapping 重复映射 | Japari Park
另外是两篇Spring原理解析参考
SpringMVC在@RequestMapping配置两个相同路径 - Text_Dexter - 博客园