传送门
Spring Cloud Alibaba系列之nacos:(1)安装
Spring Cloud Alibaba系列之nacos:(2)单机模式支持mysql
Spring Cloud Alibaba系列之nacos:(3)服务注册发现
Spring Cloud 系列之OpenFeign:(4)集成OpenFeign
从一个实际问题出发
一个微服务如何调用多个不同Feign接口?
用不同的name属性来区分不同的目标服务
回顾一下声明OpenFeign接口,里面提到微服务auth调用cipher的接口。
要调用远程feign接口,需要先按照feign接口定义,比如前面的:
@FeignClient(name = FeignConstant.CIPHER_SERVICE)
public interface AuthFeignClient
{
@GetMapping(value = "/echo/{str}")
String echo(@PathVariable("str") String str);
}
- 接口上面打上注解@FeignClient,其中FeignClient表明这是一个OpenFeign接口,里面至少要定义属性"name",对应的所要调用的服务的名称
- 声明要调用的方法,与传统的SpringMvc没有区别
如果现在微服务auth调用其它模块的接口呢?假设现在有另一个日志模块log-service
那么该怎么调用呢?首先要定义定义OpenFeign接口!从上面的原有接口来看
@FeignClient(name = FeignConstant.CIPHER_SERVICE)
public interface AuthFeignClient
{ }
因为AuthFeignClient接口上面要引入FeignClient注解,里面name定义了目标微服务的名称"cipher-service",由此显而易见,一个接口上只能定义一个目标服务,所以对于log-service只能新建一个接口LogFeignClient
public class FeignConstant
{
/** 加密服务 */
public static final String CIPHER_SERVICE = "cipher-service";
/** 日志服务 */
public static final String LOG_SERVICE = "log-service";
}
@FeignClient(name = FeignConstant.LOG_SERVICE)
public interface LogFeignClient
{
}
并在此基础上,新增一个log模块,并同样定义一个接口供auth模块调用
@RestController
public class LogController
{
@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
public String echo(@PathVariable String string)
{
return "Hello Nacos Discovery " + string;
}
}
就差一个测试类了,在auth模块新增测试方法echoLog()如下
@RestController
public class TestController
{
@Autowired
private LogFeignClient logFeignClient;
@RequestMapping(value = "/log/echo/{str}", method = RequestMethod.GET)
public String logEcho(@PathVariable String str)
{
return logFeignClient.echo(str);
}
}
浏览器请求一下,http://localhost:8080/log/echo/ssss,返回Hello Nacos Discovery ssss,表明调用成功
相同的name属性,不同的contextId来区分不同的目标服务
上面对于cipher,log服务,在auth端FeignClient定义中是用不同的name来区分的。但是这样配置取决代码组织。如果所有的FeignClient是相同的名称remote-auth-call,表示远程调用。我们来试试:
@FeignClient(name = FeignConstant.CIPHER_SERVICE, path = FeignConstant.CIPHER_SERVICE)
public interface AuthFeignClient
{
@GetMapping(value = "/echo/{str}")
String echo(@PathVariable("str") String str);
}
@FeignClient(name = FeignConstant.CIPHER_SERVICE, path = FeignConstant.LOG_SERVICE)
public interface LogFeignClient
{
@GetMapping(value = "/log/echo/{str}")
String echo(@PathVariable("str") String str);
}
-
AuthFeignClient,LogFeignClient的name名称是一样的,通过path指定服务名称
启动一下auth,很不幸会报错
The bean 'cipher-service.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
这个办法可以通过指定contextId来指定
If we want to create multiple feign clients with the same name or url so that they would point to the same server but each with a different custom configuration then we have to use
contextId
attribute of the@FeignClient
in order to avoid name collision of these configuration beans.
具体的解决方案可以参考Spring Cloud OpenFeign
配置URL直连调用
FeignClient支持客户端指定微服务调用地址url,这个功能在开发时特别有用。现代稍微大一点的项目,几乎都是多人协作开发,开发过程中,如果需要本地调试debug,通过配置url就显得很方便
@FeignClient(name = FeignConstant.LOG_SERVICE, url = "127.0.0.1:8083")
public interface LogFeignClient
{
@GetMapping(value = "/log/echo/{str}")
String echo(@PathVariable("str") String str);
}
使用SpringCloud的框架开发,一般都会用到注册发现中心,不论是官方的Eureka还是阿里的Nacos。假设张三,李四,王五同时开发一个项目,大家都启动时都会注册到Nacos(或Eureka),如果不配置url直接调用,张三本地debug时很有可能调用到其它人的机器上导致失败。
超时处理
OpenFeign组件支持配置接口调用超时!可以直接看下官方文档的说明,这里配上了中文翻译
We can configure timeouts on both the default and the named client. OpenFeign works with two timeout parameters:
connectTimeout
prevents blocking the caller due to the long server processing time.
readTimeout
is applied from the time of connection establishment and is triggered when returning the response takes too long.我们可以在默认客户端和命名客户端上配置超时。OpenFeign使用两个超时参数:
- connectTimeout防止由于服务器处理时间过长而阻塞调用者。
- readTimeout从连接建立时开始应用,并在返回响应时间过长时触发。
配置超时时间
首先在auth端的配置文件中bootstrap.properties添加上面的超时设置
feign.client.config.default.connectTimeout=1000
feign.client.config.default.readTimeout=1000
请求读取超时时间readTimeout
然后将log服务的接口改造下,手动让它休眠超过1s,比如3s钟
@RestController
public class LogController
{
@RequestMapping(value = "/log/echo/{string}", method = RequestMethod.GET)
public String echo(@PathVariable String string) throws Exception
{
Thread.sleep(3 * 1000);
return "Hello Nacos Discovery " + string;
}
}
浏览器请求一下,http://localhost:8080/log/echo/ssss,返回错误页面
通过浏览器抓取一下请求,会发现整个请求才耗时1s钟,说明客户端设置的超时时间readTimeout生效了!
请求连接超时时间connectTimeout
上面的readTimeout表示请求已经到达服务端,但是响应超过了规定时间没有返回。而connectTimeout表示客户端跟服务器建立连接的超时时间,这里要注意一点的是connectTimeout要设置的不合理,可能并不能生效从而导致设置的回调处理失败,后面会具体例子介绍
In case the server is not running or available a packet results in connection refused. The communication ends either with an error message or in a fallback. This can happen before the
connectTimeout
if it is set very low. The time taken to perform a lookup and to receive such a packet causes a significant part of this delay. It is subject to change based on the remote host that involves a DNS lookup.如果服务器未运行或不可用,数据包将导致连接被拒绝。通信以错误消息或回退结束。如果connectTimeout设置得很低,则这可能会在connectTimeout之前发生。执行查找和接收这样的分组所花费的时间导致了该延迟的很大一部分。它可能会根据涉及DNS查找的远程主机进行更改。
在系统中设置超时时间是很有必要的,如果没有或设置了不合理的超时时间,可能会导致系统的RT变高,严重时甚至系统会不可用。所以可以在很多地方:db数据源连接,httpclient调用,java的try...lock方法等都有超时设置的支持
日志处理
如果按照上面的顺序一路执行下来,细心的会发现一个问题,在远程调用时,auth后端服务并没有打印任何日志。 这里不是说程序员主动打印的业务日志,而是OpenFeign组件自带的日志。为了达到打印OpenFeign日志的目的,需要做以下处理
配置Feign调用的日志级别为DEBUG
先需要将指定的Feign接口配置日志级别为DEBUG
logging.level.project.user.UserClient: DEBUG
这里的project.user.UserClient指的就是系统里面FeignClient接口路径,比如调用log服务的feignClietn接口LogFeignClient
# feign log
logging.level.com.tw.tsm.auth.feign.LogFeignClient=DEBUG
如果有多个FeignClient,可以通过通配符来来指定,而不用一个个都列出来
logging.level.com.tw.tsm.auth.feign.*=DEBUG
增加OpenFeign配置类,指定打印的日志级别为FULL
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfiguration
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
经过上面的2步之后,浏览器请求一下,http://localhost:8080/log/echo/ssss,观察一下控制台日志,会输出类似以下日志
2023-02-11 20:31:57.450 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] ---> GET http://log-service/log/echo/ssss HTTP/1.1
2023-02-11 20:31:57.451 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] ---> END HTTP (0-byte body)
2023-02-11 20:32:00.460 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] <--- HTTP/1.1 200 (3009ms)
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] connection: keep-alive
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] content-length: 26
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] content-type: text/plain;charset=UTF-8
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] date: Sat, 11 Feb 2023 12:32:00 GMT
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] keep-alive: timeout=60
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo]
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] Hello Nacos Discovery ssss
2023-02-11 20:32:00.461 DEBUG 8012 --- [nio-8080-exec-3] com.tw.tsm.auth.feign.LogFeignClient : [LogFeignClient#echo] <--- END HTTP (26-byte body)
从日志中可以看到
- 请求类型,为GET
- 请求URL, http://log-service/log/echo/ssss,里面包括了请求参数ssss
- 返回响应Hello Nacos Discovery ssss
- 以及请求的时间等等
日志的级别含义
如果要指明日志打印的详细程度,可以通过Logger.Level来指定。在刚才的例子中是配置的FULL,表示的打印所有的请求,包括:记录请求和响应的标头、正文和元数据。除此以外,还有其它三种
- NONE,默认的级别,表示不打印
- BASIC,仅记录请求方法和URL以及响应状态代码和执行时间
- HEADERS,记录基本信息以及请求和响应标头
OpenFeign 降级
OpenFeign 支持对服务降级的(可以参考1.5. Feign Spring Cloud CircuitBreaker Support),不过由于引入的版本是最新的,已经默认不支持Hystrix了(可以参考1.4. Feign Hystrix Support)
If Spring Cloud CircuitBreaker is on the classpath and
spring.cloud.openfeign.circuitbreaker.enabled=true
, Feign will wrap all methods with a circuit breaker.
所以手动引入hystrix 项目的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<exclusions>
<!-- 移除Ribbon负载均衡器,避免冲突 -->
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
降级处理
OpenFeign支持回调函数,比如改造上面的LogFeignClient,增加降级配置fallback
@FeignClient(name = FeignConstant.LOG_SERVICE, fallback = LogFeignClientFallback.class)
public interface LogFeignClient
{
@GetMapping(value = "/log/echo/{str}")
String echo(@PathVariable("str") String str);
}
而fallback对应的LogFeignClientFallback,就是LogFeignClient远程feign调用失败后执行的函数
@Component
public class LogFeignClientFallback implements LogFeignClient {
@Override
public String echo(String str) {
return "fail";
}
}
通过上面配置的超时例子测试一下, 浏览器请求一下,http://localhost:8080/log/echo/ssss,返回“fail”字符串,实现了服务的降级了,更多用法可以参考文档研究一下
继承支持
通过对openFeign的介绍及使用的例子,现在已经能感受到它的方便了。那还能再方便一点吗?可以的,OpenFeign官方支持feign接口的继承特性Feign Inheritance Support,以便进一步简化它的调用,具体什么意思呢?还是以上面的例子来说明!
按照前面调用的关系,现在的类图应该是下面这样的
那么这个类图的问题在于哪里呢?
- 对于每一个feignClient接口,除了要在调用方声明,比如请求URL,请求方式,请求参数
- 还必须要在被调用方,也同样声明一遍
为此,OpenFeign提供了继承特性,就是将接口声明从调用方抽象出来,让被调用方直接实现该接口来达到减化编码的目的,现在改造一下上面的AuthFeignClient
增加openFeign接口模块feign-api,及下面的cipher-api和log-api
然后将原来auth-service下面定义的删除,引入cipher-api模块
<dependency>
<groupId>com.tw.tsm</groupId>
<artifactId>cipher-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在cipher-service模块改造一下原来的Controller,让实现CipherFeignClient,并移除RequestMapping配置信息
@RestController
public class EchoController implements CipherFeignClient
{
public String echo(@PathVariable String string)
{
return "Hello Nacos Discovery " + string;
}
}
重新发起调用会发现效果是一样,这样就达到了利用feign接口定义的目的