问题描述
最近笔者进行 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.<init>(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。
关于在 Windows 上远程对 Linux 进行抓包的方法,可见笔者的另一篇博客:
在 Windows 上远程对 Linux 进行抓包:
https://blog.csdn.net/wangpaiblog/article/details/132729142 -
到此,整个原因就已经明白了。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 会调用失败。这是一个笔者反复实验之后,令笔者非常意外不解的潜规则。
-
RestTemplate 需要
@LoadBalanced
注解,所以也需要如下相应的依赖,但此依赖在以前是不需要提供的。另外,此依赖的版本需要和 Spring Cloud 的相一致。<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency>
在父 POM 使用 Spring Cloud 依赖管理后,上述依赖可以不给出版本号。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Spring Cloud 的版本号</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
对于 OpenFeign
这是因为笔者使用了 Maven 多模块,从而 OpenFeign 接口与 Spring Boot 启动类不在同一个模块,也不在同一个包中。此时需要在 Spring Boot 启动类的 @EnableFeignClients
注解使用属性 basePackages 来指定基包。
@EnableFeignClients(basePackages = {"xxx.xxx"})
此时如果不指定基包,Spring Boot 会直接将 @FeignClient
的 fallback 类注入到 @FeignClient
接口中,从而导致 OpenFeign 只会无声调用 fallback 类。