最近笔者进行 Spring Cloud Alibaba 版本升级的时候,发生了一个奇怪的事情:Nacos 显示服务已注册,但 RestTemplate 和 OpenFeign 的调用却一直失败。
具体来说,笔者的两个服务,均在 Nacos 网页管理页面中显示各自的服务名,但一个服务使用 OpenFeign 调用另一个服务时,一直失败,OpenFeign 的 fallback 类方法一直被触发,而且没有抛出任何异常。但是,笔者又分别使用远程 Postman 和本地 curl 命令测试了这些接口,却显示这些接口都是正常的。
笔者百思不得其解,多次尝试失败之后,只好和以前一样,使用更基本的技术来测试。结果笔者发现在使用 RestTemplate 调用接口时,如果 URL 是服务名,此调用也会失败,但如果 URL 是真实的普通网页 URL,则调用会成功。RestTemplate 报错内容如下。
笔者报错时的运行环境:
Spring Cloud Alibaba:2022.0.0.0-RC2
Spring Cloud:2022.0.0
Spring Boot:3.0.2
Nacos 2.2.3
Maven 3.8.3
JDK 17.0.7
IntelliJ IDEA 2022.3.1 (Ultimate Edition)
笔者原来运行正常时的运行环境:
Spring Cloud Alibaba:2.1.2.RELEASE
Spring Cloud:2.1.15.RELEASE
Spring Boot:Greenwich.SR6
Nacos 2.2.3
Maven 3.8.3
JDK 1.8
IntelliJ IDEA 2023.1.1 (Ultimate Edition)
202X-XX-XX XX:XX:XX.XXX [ERROR] [http-nio-XXX-exec-1] org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:175)
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://XXX/xxx": XXX] with root cause
java.net.UnknownHostException: XXX
at sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567) ~[?:?]
at java.net.Socket.connect(Socket.java:633) ~[?:?]
at java.net.Socket.connect(Socket.java:583) ~[?:?]
at sun.net.NetworkClient.doConnect(NetworkClient.java:183) ~[?:?]
at sun.net.www.http.HttpClient.openServer(HttpClient.java:532) ~[?:?]
at sun.net.www.http.HttpClient.openServer(HttpClient.java:637) ~[?:?]
at sun.net.www.http.HttpClient.(HttpClient.java:280) ~[?:?]
at sun.net.www.http.HttpClient.New(HttpClient.java:385) ~[?:?]
at sun.net.www.http.HttpClient.New(HttpClient.java:407) ~[?:?]
at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1309) ~[?:?]
at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1242) ~[?:?]
at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1128) ~[?:?]
at sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:1057) ~[?:?]
at org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:75) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:862) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:378) ~[spring-web-6.0.4.jar:6.0.4]
at org.wangpai.xixihaha.serverprovider.service.UserService.searchUserByUserId(UserService.java:50) ~[UpRqu6ZoXB/:?]
at org.wangpai.xixihaha.serverprovider.controller.UserController.login(UserController.java:58) ~[UpRqu6ZoXB/:?]
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?]
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:568) ~[?:?]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[spring-webmvc-6.0.4.jar:6.0.4]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.0.4.jar:6.0.4]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731) ~[tomcat-embed-core-10.1.5.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.0.4.jar:6.0.4]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814) ~[tomcat-embed-core-10.1.5.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.4.jar:6.0.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.4.jar:6.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-10.1.5.jar:10.1.5]
at java.lang.Thread.run(Thread.java:833) [?:?]
笔者刚开始认为这是 Spring Cloud 版本不匹配的原因,毕竟这里有三个独立的依赖:Spring Cloud Alibaba、Spring Cloud、Spring Boot。于是笔者查看了 Spring Cloud Alibaba 官方的版本对照表,结果使用官方的版本配置,上述问题依然存在。
Spring Cloud Alibaba 官方版本对照表网址:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
然后,笔者对程序每一个地方都插入了日志,运行的日志表明,使用 RestTemplate 和 OpenFeign 时,理应被调用的那个服务完全没有感知到被调用。
之后,笔者使用了 Wireshark 进行远程抓包。抓包结果表明,RestTemplate 的进行调用时,DNS 报文显示无法解析这个 URL。
到此,整个原因就已经明白了。RestTemplate 和 OpenFeign 并没有感知到理应被调用的那个服务,因此调用失败。
因为有 Nacos 的存在,所以在使用 RestTemplate 和 OpenFeign 的时候应该是不会出现 DNS 报文的。因为所有的服务名是由 Nacos 来解析,DNS 是不可能可以解析到微服务的服务名的。而 Nacos 会自动同步服务名对应的 URL 给每一个服务,所以正常情况下,使用 RestTemplate 的那个服务会直接解析被调用服务的 URL,根本不需要 DNS 报文。
那么,问题究竟出现在哪里呢?由于 Nacos 的网页管理页面没有显示错误,笔者认为这应该是 RestTemplate 的问题。另一个理由是,笔者的 Nacos 是最新版的,而上述错误出现在最新版的 Spring Cloud,这里不符合常理的。因为最新版的 Nacos 不应该对最新版的 Spring Cloud 不支持,反而对很旧版的 Spring Cloud 提供支持。因此,问题不出在 Nacos 上。
考虑到 Spring Boot 3 开始发生的重大变更,笔者猜测这应该是 Spring Cloud 对某些包进行了拆分,而且由于底层使用的是反射和动态代理,因此这种缺失只在运行时才能被感知。
服务名不能使用下划线 _
,否则 RestTemplate 会调用失败。
这是一个笔者反复实验之后,令笔者非常意外不解的潜规则。
RestTemplate 需要 @LoadBalanced
注解,所以也需要如下相应的依赖,但此依赖在以前是不需要提供的。另外,此依赖的版本需要和 Spring Cloud 的相一致。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-loadbalancerartifactId>
dependency>
在父 POM 使用 Spring Cloud 依赖管理后,上述依赖可以不给出版本号。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Spring Cloud 的版本号version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
这是因为笔者使用了 Maven 多模块,从而 OpenFeign 接口与 Spring Boot 启动类不在同一个模块,也不再同一个包中。此时需要在 Spring Boot 启动类的 @EnableFeignClients
注解使用属性 basePackages 来指定基包。
@EnableFeignClients(basePackages = {"xxx.xxx"})
此时如果不指定基包,Spring Boot 会直接将 @FeignClient
的 fallback 类注入到 @FeignClient
接口中,从而导致 OpenFeign 只会无声调用 fallback 类。