【Springcloud】(06)中级搭建-OpenFeign服务调用
【一】OpenFeign是什么
(1.1)Feign和OpenFeign
Feign是一个声明式WebService客户端,使用Feign能让编写WebService客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。
Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用,只不过对原有的方式(Ribbon+RestTemplate)进行了封装,开发者不必手动使用RestTemplate调服务,而是定义一个接口,在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,简化了开发。
Springcloud对Feign进行了封装,使其可以支持SpringMVC标准注解和HttpMessageConverters。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。Feign可以与Nacos和Ribbon组合使用以支持负载均衡。
(1.2)Feign能干什么?
以前是使用Ribbon和RestTemplate,利用RestTemplate对http请求的封装处理,形成一套模板化的调用方法,但是在这实际开发中,对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个服务自行封装一些客户端类来包装这些依赖服务的调用。
所以,Feign在此基础上做了进一步的封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解方式来配置它(以前是DAO接口上标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解就可以了)
也就是说可以完成对服务提供方的接口绑定,简化了使用Springcloud Ribbon时,自动封装服务调用客户端的开发量
(1.3)Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。
但是和Ribbon不同的是,通过Feign只需要定义服务绑定接口并且以声明式的方法,优雅而简单的实现了服务调用。
【二】Nacos结合OpenFeign使用Demo
【1】OpenFeign服务调用
(1)创建新的module:cloudalibaba-consumer-nacos-feign84,修改pom
加入openfeign的依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)修改yml配置文件
server:
port: 84 # 指定当前服务的端口号为84
spring:
application:
name: nacos-feign-order-consumer # 服务名称为nacos-feign-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册到服务中心8848端口
(3)启动类上添加注解@EnableFeignClients
不加这个注解的话,在注入Service接口的时候会出现红线
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerMain84 {
public static void main(String[] args) {
SpringApplication.run(ConsumerMain84.class,args);
}
}
(4)创建Service接口
使用@FeignClient注解
整合了SpringMVC,所以可以搭配SpringMVC的注解@RequestMapping之类的一起使用
@FeignClient(name = "nacos-payment-provider",path = "/stock")
public interface StockFeignService {
// 生命需要调用的rest解接口对应的方法
@RequestMapping("reduct")
public String reduct();
}
(5)创建Controller
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
StockFeignService stockFeignService;
@RequestMapping("/add")
public String add() {
System.out.println("下单成功!");
String msg = stockFeignService.reduct();
return "Hello Feign,"+msg;
}
}
(6)启动生产者服务9001和9002,消费者服务84
两个服务的端口分别是9001和9002,但是两个服务的服务名都是”nacos-payment-provider“。
两个服务中都有相同的Controller接口
@RestController
@RequestMapping("/stock")
public class ReduceController {
@Value("${server.port}") // 9001和9002
private String serverPort;
@RequestMapping("/reduct")
public String reduct() throws InterruptedException
{
System.out.println("扣减库存");
return "扣减库存的端口号为: "+ serverPort;
}
}
启动成功后看Nacos页面的服务列表
(7)测试:http://localhost:84/order/add
访问测试,可以看到已经实现远程服务调用,并且会在9001端口的服务和9002端口的服务之间轮询调用,实现了负载均衡
【2】@FeignClient注解的参数介绍
(1)value, name
value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
(2)contextId
比如我们有个user服务,但user服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:
Client 1
@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id") int id);
}
Client 2
@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
@GetMapping("/user2/get")
public User getUser(@RequestParam("id") int id);
}
这种情况下启动就会报错了,因为Bean的名称冲突了,具体错误如下:
Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
另一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。
可以看到如果配置了contextId就会用contextId,如果没有配置就会去value然后是name最后是serviceId。默认都没有配置,当出现一个服务有多个Feign Client的时候就会报错了。
(3)url
url用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。
@FeignClient(name = "optimization-user", url = "http://localhost:8085")
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id") int id);
}
(4)decode404
当调用请求发生404错误时,decode404的值为true,那么会执行decoder解码,否则抛出异常。抛异常的话就是异常信息了,如果配置了fallback那么就会执行回退逻辑。
(5)configuration
configuration是配置Feign配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等。
@FeignClient(value = "optimization-user", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
(6)fallback
定义容错的处理类,也就是回退逻辑,fallback的类必须实现Feign Client的接口,无法知道熔断的异常信息。
fallback定义
@Component
public class UserRemoteClientFallback implements UserRemoteClient {
@Override
public User getUser(int id) {
return new User(0, "默认fallback");
}
}
使用示列
@FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
(7)fallbackFactory
也是容错的处理,可以知道熔断的异常信息。
fallbackFactory定义
@Component
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
@Override
public UserRemoteClient create(Throwable cause) {
return new UserRemoteClient() {
@Override
public User getUser(int id) {
logger.error("UserRemoteClient.getUser异常", cause);
return new User(0, "默认");
}
};
}
}
使用示列
@FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {
@GetMapping("/user/get")
public User getUser(@RequestParam("id")int id);
}
(8)path
path定义当前FeignClient访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是user, 那么具体方法上的路径就只需要写/get 即可。
@FeignClient(name = "optimization-user", path="user")
public interface UserRemoteClient {
@GetMapping("/get")
public User getUser(@RequestParam("id") int id);
}
(9)primary
primary对应的是@Primary注解,默认为true,官方这样设置也是有原因的。当我们的Feign实现了fallback后,也就意味着Feign Client有多个相同的Bean在Spring容器中,当我们在使用@Autowired进行注入的时候,不知道注入哪个,所以我们需要设置一个优先级高的,@Primary注解就是干这件事情的。
(10)qualifier
qualifier对应的是@Qualifier注解,使用场景跟上面的primary关系很淡,一般场景直接@Autowired直接注入就可以了。
如果我们的Feign Client有fallback实现,默认@FeignClient注解的primary=true, 意味着我们使用@Autowired注入是没有问题的,会优先注入你的Feign Client。
如果你鬼斧神差的把primary设置成false了,直接用@Autowired注入的地方就会报错,不知道要注入哪个对象。
解决方案很明显,你可以将primary设置成true即可,如果由于某些特殊原因,你必须得去掉primary=true的设置,这种情况下我们怎么进行注入,我们可以配置一个qualifier,然后使用@Qualifier注解进行注入,示列如下:
Feign Client定义
@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
@GetMapping("/get")
public User getUser(@RequestParam("id") int id);
}
Feign Client注入
@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;
【3】openFeign如何传参?
(1)传递JSON数据
这个也是接口开发中常用的传参规则,在Spring Boot 中通过@RequestBody标识入参。
provider接口中JSON传参方法如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order2")
public Order createOrder2(@RequestBody Order order){
return order;
}
}
consumer中openFeign接口中传参代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,这里的@RequestBody可以不填
* 方法名称任意
*/
@PostMapping("/openfeign/provider/order2")
Order createOrder2(@RequestBody Order order);
}
注意:openFeign默认的传参方式就是JSON传参(@RequestBody),因此定义接口的时候可以不用@RequestBody注解标注,不过为了规范,一般都填上。
(2)POJO表单传参
这种传参方式也是比较常用,参数使用POJO对象接收。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order1")
public Order createOrder1(Order order){
return order;
}
}
consumer消费者openFeign代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,如果通过POJO表单传参的,使用@SpringQueryMap标注
*/
@PostMapping("/openfeign/provider/order1")
Order createOrder1(@SpringQueryMap Order order);
}
网上很多人疑惑POJO表单方式如何传参,官方文档明确给出了解决方案,如下:
openFeign提供了一个注解@SpringQueryMap完美解决POJO表单传参。
(3)URL中携带参数
此种方式针对restful方式中的GET请求,也是比较常用请求方式。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@GetMapping("/test/{id}")
public String test(@PathVariable("id")Integer id){
return "accept one msg id="+id;
}
consumer消费者openFeign接口如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
@GetMapping("/openfeign/provider/test/{id}")
String get(@PathVariable("id")Integer id);
}
使用注解@PathVariable接收url中的占位符,这种方式很好理解。
(4)普通表单参数
此种方式传参不建议使用,但是也有很多开发在用。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/test2")
public String test2(String id,String name){
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
}
consumer消费者openFeign接口传参如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 必须要@RequestParam注解标注,且value属性必须填上参数名
* 方法参数名可以任意,但是@RequestParam注解中的value属性必须和provider中的参数名相同
*/
@PostMapping("/openfeign/provider/test2")
String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2);
}
(5)!!!@SpringQueryMap实现多个实体类传参
@SpringQueryMap是微服务之间调用,使用openFeign通过get请求方式来处理多个入参(多个实体类参数)情况的注解,多用于restful风格方式。
@SpringQueryMap的作用就是把实体转化成表单数据,例如:
{
"username" : "zhangsan",
"passwd" : "******"
}
通过@SpringQueryMap标注之后呢,会变成这样子
url?username=zhangsan&passwd=******
注意:被@SpringQueryMap注解的对象只能有一个。因为不能保证多个对象中是否会存在相同的属性名,这是值得注意的一点。
(1)单实体入参数
feign调用方
@GetMapping(value = "/xx/xxxx")
ApiResponse<SysUserQueryResponse> query(@SpringQueryMap SysUserQueryRequest queryRequest);
被调用方
@GetMapping
@Operation(summary = "查询用户")
public ApiResponse<SysUserQueryResponse> query(SysUserQueryRequest queryRequest) {
return ApiResponse.ok(sysUserReadModelRepo.query(queryRequest));
}
(2)多实体入参数
错误的方式:以下这两种方式都是错误的,都是只能生效一个实体的传参数
feign调用方
@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@RequestParam Pagination pagination, @SpringQueryMap SysUserQueryRequest queryRequest);
@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@SpringQueryMap Pagination pagination, @SpringQueryMap SysUserQueryRequest queryRequest);
正确的方式:根据上面的结论,可知只会生效一个实体的传参,其原因是因为get方式只能是表单提交的,不能通过body传输,如果这两个实体存在相同的属性,就会出现问题,所以就默认不会取第二个实体来传参数。
解决方法也很简单,将这两个实体都转为map,放到一个map中即可,再次提醒这两个实体中不能存在相同的属性名,否则出现参数覆盖情况
feign调用方
## 实体转map,使用阿里巴巴库的JSON类
Map<String, Object> param = JSON.parseObject(JSON.toJSONString(Pagination), Map.class);
Map<String, Object> param1 = JSON.parseObject(JSON.toJSONString(SysUserQueryRequest), Map.class);
param.addAll(param1);
## 以param作为参数传递
@GetMapping(value = "/xx/xxxx")
ApiPageResponse<SysUserQueryResponse> query(@SpringQueryMap Map<String, Object> param);
被调用方,无需用map接收,可以随意对象,只需要对应上属性名和类型即可
@GetMapping
@Operation(summary = "分页查询用户")
public ApiPageResponse<SysUserQueryResponse> query(Pagination pagination, @Valid SysUserQueryRequest queryRequest) {
return ApiPageResponse.ok(pagination, sysUserReadModelRepo.query(pagination, queryRequest));
}
【4】服务调用的更多案例
(1)案例一
本案例演示ContextId参数的使用、path参数的使用、远程调用的参数使用
(1)首先在9001和9002两个服务里加上新的接口PaymentController
注意这里没有使用@RequestMapping在PaymentController上加统一的path前缀
@RestController
public class PaymentController {
@Value("${server.port}") // 9001
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "访问的服务端口号: "+ serverPort+"\t ,传递的参数id:"+id;
}
}
(2)然后在84端口的服务里加上新的Service接口PaymentFeignService
@FeignClient(name = "nacos-payment-provider")
public interface PaymentFeignService {
// 声明需要调用的rest解接口对应的方法
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id);
}
(3)然后在84端口的服务里修改OrderController
注入了新的PaymentFeignService对象,添加了新的payment方法,并且传递参数为id
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
StockFeignService stockFeignService;
@Autowired
PaymentFeignService paymentFeignService;
@RequestMapping("/add")
public String add() {
System.out.println("下单成功!");
String msg = stockFeignService.reduct();
return "Hello Feign,"+msg;
}
@RequestMapping("/payment/{id}")
public String payment(@PathVariable("id") Integer id) {
System.out.println("查询成功!");
String msg = paymentFeignService.getPayment(id);
return "Hello Feign,"+msg;
}
}
(4)然后84端口服务启动报错了,分析原因
报错内容为
Description:
The bean 'nacos-payment-provider.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
这里就是因为两个不同Service接口里都配置了@FeignClient(name = “nacos-payment-provider”),name一致导致Bean的名称冲突了
在@FeignClient注解里添加参数contextId,并且指定不同的名称
@FeignClient(contextId = "payment",name = "nacos-payment-provider")
public interface PaymentFeignService {
// 声明需要调用的rest解接口对应的方法
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id);
}
@FeignClient(contextId = "stock",name = "nacos-payment-provider",path = "/stock")
public interface StockFeignService {
// 生命需要调用的rest解接口对应的方法
@RequestMapping("reduct")
public String reduct();
}
再重新启动,没有报错了
(5)测试:http://localhost:84/order/payment/2
【5】日志配置
(1)介绍
Feign提供了很多的扩展机制,让用户可以更加灵活的使用
有时候我们遇到Bug,比如接口调用失败、参数没收到等问题,或者想看看调用性能,就需要配置Feign的日志了,以此让Feign把请求信息输出来。
日志的级别有4种,分别是:
(1)None【性能最佳,适用于生产】:不记录任何日志(默认值)
(2)BASIC【适用于生产环境追踪问题】:仅记录请求方法、URL、响应状态代码以及执行时间
(3)HEADERS:记录BASIC级别的基础上,记录请求和响应的header
(4)FULL【比较适用于开发及测试环境定位问题】
日志的配置又分为局部配置和全局配置
(1)局部配置:让调用的微服务生效,在@FeignClient注解中指定使用的配置类
(2)全局配置:在yml配置文件中执行Client的日志级别才能正常输出日志,格式是“logging.level.feign接口包路径=debug”
(2)全局日志配置
(1)定义一个配置类,指定日志级别
这里指定的是FULL
/**
* @ClassName: FeignConfig
* @Author: AllenSun
* @Date: 2020/4/13 19:39
*
* 全局配置:当使用@Configuration,会把配置作用在所有的服务提供方
* 局部配置:如果只想针对某一个服务进行配置,就不要加@Configuration
*/
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
(2)修改yml配置文件
开启springboot自带的日志,springboot默认的日志级别是info,feign的debug日志级别就不会输出
logging:
level:
com.allen.springcloud.feign: debug # 只对feign目录下的文件生效,否则显示全部日志会特别多
(3)测试一下:http://localhost:84/order/payment/2
(3)局部日志配置
(1)把配置类的注解@Configuration删掉
这个注解的作用就是给全部的服务自动配置,去掉以后就需要我们自己手动配置了,哪里需要就在哪里配置
/**
* @ClassName: FeignConfig
* @Author: AllenSun
* @Date: 2020/4/13 19:39
*
* 全局配置:当使用@Configuration,会把配置作用在所有的服务提供方
* 局部配置:如果只想针对某一个服务进行配置,就不要加@Configuration
*/
// @Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
(2)yml配置跟上面一样,不变
(3)在PaymentFeignService了加Configuration配置,让其显示日志,而StockFeignService不加
@FeignClient(contextId = "payment",
name = "nacos-payment-provider",
configuration = FeignConfig.class)
public interface PaymentFeignService {
// 声明需要调用的rest解接口对应的方法
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id);
}
@FeignClient(contextId = "stock",
name = "nacos-payment-provider",
path = "/stock")
public interface StockFeignService {
// 生命需要调用的rest解接口对应的方法
@RequestMapping("reduct")
public String reduct();
}
(4)测试一下
访问:http://localhost:84/order/payment/2(应该有日志)
访问:http://localhost:84/order/add(应该没有日志)
(4)通过yml配置局部日志
feign:
client:
config:
nacos-payment-provider:
loggerLevel: BASIC
测试访问http://localhost:84/order/add(有日志了)
【6】契约配置
【7】超时时间配置和熔断降级
Feign本身也集成Hystrix熔断器,starter内查看。
(1)在yml配置文件中配置feign的请求超时时间,如果超时会报错
(2)在yml配置文件中配置的开启feign熔断器Hystrix支持,默认是关闭的
(3)编写FallBack降级处理类,记得是实现对应的FeignClient客户端
(4)在@FeignClient注解中,指定FallBack处理类
(5)测试服务降级的效果
(1)yml配置feign的请求超时时间
(2)yml配置开启feign熔断器Hystrix支持
(3)编写FallBack降级处理类
@Component
public class PaymentFeignServiceImpl implements PaymentFeignService {
@Override
public String getPayment(Integer id) {
return "网络异常,请求超时,请稍后再试...";
}
}
(4)@FeignClient注解中指定FallBack处理类
(5)测试一下
修改被调用的方法,设置sleep
修改9001端口的方法sleep 8秒,而9002端口的不改
@RestController
public class PaymentController {
@Value("${server.port}") // 9001
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "访问的服务端口号: "+ serverPort+"\t ,传递的参数id:"+id;
}
}
访问测试一下:http://localhost:84/order/payment/6
如果是请求到9001,会返回超时信息
如果是请求到9002,一切正常
【三】OpenFeign实现源码原理
【1】提出问题
OpenFeign其实还是负责通信的,在使用过程中,让开发者可以面向接口开发的方式进行远程调用,那么:
(1)OpenFeign的注解时如何被扫描解析的呢?
通过实现ImportBeanDefinitionRegistrar,然后注入实例到IOC容器
(2)注解标注的接口注入到IOC中的实例是什么?
就是一个JDK的动态代理
(3)OpenFeign的上下文是如何构建的呢?
(4)远程通信是如何实现的呢?与RestTemplate有关系吗?
其实就是这个类LoadBalanceFeignClient,这里用到了委派模式,delegate在默认情况下是feign.Client$Default,里面的通信方式是HttpURLConnection,可以切换到HttpClient或者OkHttp3。RestTemplate默认也是基于HttpURLConnection封装的一个工具类,Feign中没有直接使用RestTemplate
带着上面的问题,准备开始Feign的源码,同样的Spring的生态下,切入点还是那几个,EnableFeignClients和FeignAutoConfiguration,其实看一下spring-cloud-openfeign-core里面的spring.factories就发现这里面其实有很多AutoConfiguration
【2】FeignClient的bean注册过程
(1)@EnableFeignClients
想要集成 Feign 客户端,需要我们通过注解 @EnableFeignClients 来开启。这个注解开启了FeignClient的解析过程。这个注解的声明如下,它用到了一个@Import注解,我们知道Import是用来导入一个配置类的,接下来去看一下FeignClientsRegistrar的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar,它是一个动态注入bean的接口,Spring Boot启动的时候,会去调用这个类中的registerBeanDefinitions来实现动态Bean的装载。它的作用类似于ImportSelector。
(2)FeignClientsRegistrar类
然后就会进入 FeignClientsRegistrar# registerBeanDefinitions 。registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClientBeanDeifinition 添加到 spring 容器中。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
//注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
//在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
//所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
这里面需要重点分析的就是 registerFeignClients 方法,这个方法主要是扫描类路径下所有的@FeignClient注解,然后进行动态Bean的注入。它最终会调用 registerFeignClient 方法。
public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
//获取注解
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
// 遍历配置的扫描包路径
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注册Feign 客户端
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
registerFeignClient 在这个方法中,就是去组装BeanDefinition,也就是Bean的定义,然后注册到Spring IOC容器。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 省略代码.....
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
我们关注一下,BeanDefinitionBuilder是用来构建一个BeanDefinition的,它是通过 genericBeanDefinition 来构建的,并且传入了一个FeignClientFactoryBean的类,代码如下。
/**
* Create a new {@code BeanDefinitionBuilder} used to construct a {@link GenericBeanDefinition}.
* @param beanClass the {@code Class} of the bean that the definition is being created for
*/
public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
builder.beanDefinition.setBeanClass(beanClass);
return builder;
}
(3)动态代理过程
我们可以发现,FeignClient被动态注册成了一个FactoryBean。
Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会把所有的FeignClient的BeanDefinition设置为FeignClientFactoryBean类型,而FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。
简单来说,FeignClient标注的这个接口,会通过FeignClientFactoryBean.getObject()这个方法获得一个代理对象。
FeignClientFactoryBean.getObject:getObject调用的是getTarget方法,它从applicationContext取出FeignContext,FeignContext继承了NamedContextFactory,它是用来统一维护feign中各个feign客户端相互隔离的上下文。
FeignContext注册到容器是在FeignAutoConfiguration上完成的。
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations 的来源就是在前面registerFeignClients方法中将@FeignClient的配置 configuration。
接着,构建feign.builder,在构建时会向FeignContext获取配置的Encoder,Decoder等各种信息。FeignContext在上文中已经提到会为每个Feign客户端分配了一个容器,它们的父容器就是spring容器。
配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。实际上他们最终调用的是Target.target()方法。
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
//实例化Feign上下文对象FeignContext
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);//构建Builder对象
//如果url为空,则走负载均衡,生成有负载均衡功能的代理类
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//如果指定了url,则生成默认的代理类
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}//生成默认代理类
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
loadBalance :生成具备负载均衡能力的feign客户端,为feign客户端构建起绑定负载均衡客户端。
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
Client client = (Client)this.getOptional(context, Client.class); 从上下文中获取一个 Client,默认是LoadBalancerFeignClient。它是在FeignRibbonClientAutoConfiguration这个自动装配类中,通过Import实现的
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
.....
}
这里的通过 DefaultFeignLoadBalancedConfiguration 注入客户端 Client 的实现
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
clientFactory);
}
}
接下去进入 targeter.target(this, builder, context, target) ,携带着构建好的这些对象去创建代理实例 ,这里有两个实现 HystrixTargeter 、DefaultTargeter 很显然,我们没有配置 Hystrix ,这里会走 DefaultTargeter
class DefaultTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
return feign.target(target);
}
}
然后会来到 feign.Feign.Builder#target(feign.Target)
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
最终会调用 ReflectiveFeign.newInstance
这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。
从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。
public <T> T newInstance(Target<T> target) {
// 解析接口注解信息
//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
// 根据方法类型
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
// 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
targetToHandlersByName.apply(target) :根据Contract协议规则,解析接口类的注解信息,解析成内部表现:targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。
public Map<String, MethodHandler> apply(Target target) {
List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate =
new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else if (md.bodyIndex() != null) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else {
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
}
if (md.isIgnored()) {
result.put(md.configKey(), args -> {
throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
});
} else {
result.put(md.configKey(),
factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
}
}
return result;
}
(4)SpringMvcContract
当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务。
【3】OpenFeign调用过程
OpenFeign调用过程图示:
在前面的分析中,我们知道OpenFeign最终返回的是一个 ReflectiveFeign.FeignInvocationHandler 的对象。那么当客户端发起请求时,会进入到 FeignInvocationHandler.invoke 方法中,这个大家都知道,它是一个动态代理的实现。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
// 利用分发器筛选方法,找到对应的handler 进行处理
return dispatch.get(method).invoke(args);
}
而接着,在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下。
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个 executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
//转化为Http请求报文
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
//发起远程通信
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
//获取返回结果
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
// .......
}
经过上面的分析,这里的 client.execute 的 client 的类型是LoadBalancerFeignClient,这里就很自然的进入 LoadBalancerFeignClient#execute。
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
其实这个execute里面得流程就是 Ribbon 的那一套。我们可以简单的看一下。首先是构造URI,构造RibbonRequest,选择 LoadBalance,发起调用。
来看一下lbClient 选择负载均衡器的时候做了什么
public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName);
if (client != null) {
return client;
}
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
ServerIntrospector.class);
client = this.loadBalancedRetryFactory != null
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
this.loadBalancedRetryFactory)
: new FeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client);
return client;
}
可以得出的结论就是 this.factory.getLoadBalancer(clientName) 跟Ribbon 源码里的获取方式一样,无疑这里获取的就是默认的 ZoneAwareLoadBalancer。然后包装成一个 FeignLoadBalancer 进行返回
既然负载均衡器选择完了,那么一定还有个地方通过该负载去选择一个服务,接着往下看:
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
上面这段代码就是通过获取到的负载进行执行请求,但是这个时候 服务还没有选择,我们跟进去 submit 请求看一看究竟:
public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext();
// .........
Observable<T> o =
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
//........
});
// .......
}
可以看到这里有个 selectServer的方法 ,跟进去:
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
String host = null;
int port = -1;
if (original != null) {
host = original.getHost();
}
if (original != null) {
Pair<String, Integer> schemeAndPort = deriveSchemeAndPortFromPartialUri(original);
port = schemeAndPort.second();
}
// Various Supported Cases
// The loadbalancer to use and the instances it has is based on how it was registered
// In each of these cases, the client might come in using Full Url or Partial URL
ILoadBalancer lb = getLoadBalancer();
if (host == null) {
// ............
} else {
// ...........if (shouldInterpretAsVip) {
Server svc = lb.chooseServer(loadBalancerKey);
if (svc != null){
host = svc.getHost();
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Invalid Server for :" + svc);
}
logger.debug("using LB returned Server: {} for request: {}", svc, original);
return svc;
} else {
// just fall back as real DNS
logger.debug("{}:{} assumed to be a valid VIP address or exists in the DNS", host, port);
}
} else {
// consult LB to obtain vipAddress backed instance given full URL
//Full URL execute request - where url!=vipAddress
logger.debug("Using full URL passed in by caller (not using load balancer): {}", original);
}
}
// ..........
return new Server(host, port);
}
可以看到的是这里获取到了之前构造好的 ZoneAwareLoadBalancer 然后调用 chooseServer 方法获取server ,这个是跟Ribbon 中是一样的流程,这里就不赘述了。
获取到了server 后,会回调先前 executeWithLoadBalancer 方法里构造的 ServerOperation 的 call 方法:
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
然后会执行 AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig) 进行最后的调用,实际上这里走的是 FeignLoadBalancer#execute
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
而这里调用的 request.client().execute(request.toRequest(), options) 则是 DefaultFeignLoadBalancedConfiguration 注入的 LoadBalancerFeignClient ,在构造 LoadBalancerFeignClient 的时候 ,传递了个 feign.Client.Default ,然后利用 feign.Client.Default 构造了一个 RibbonRequest。
所以这里走 feign.Client.Default#execute :
@Override
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
利用 JDK 提供的 HttpURLConnection 发起远程的 HTTP通讯。至此发起请求的流程就完成了。下面附上一张这个过程的流程图,对于Ribbon的调用过程请参考 :Ribbon 源码分析。
【4】OpenFeign Configuration
针对 feign 的 Configuration ,官方给我们提供了很多的个性化配置,具体可以参考 org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration
public static class FeignClientConfiguration {
// 日志
private Logger.Level loggerLevel;
// 连接超时
private Integer connectTimeout;
private Integer readTimeout;
//重试
private Class<Retryer> retryer;
//解码
private Class<ErrorDecoder> errorDecoder;
private List<Class<RequestInterceptor>> requestInterceptors;
// 编码
private Boolean decode404;
private Class<Decoder> decoder;
private Class<Encoder> encoder;
// 解析
private Class<Contract> contract;
private ExceptionPropagationPolicy exceptionPropagationPolicy;
}
这里举个简单的例子,以Logger 为例。我们想为每个不同的 FeignClient 设置日志级别。
1.添加配置类:
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
2.配置日志级别 ,logging.level + FeignClient 包的全路径。
logging.level.com.xxx.xxxService: DEBUG
就这样就配置完成了。重启服务就可以看到效果。
【5】总结
(1)通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描
(2)解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
(3)Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy.
(4)基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作。
【四】Eureka结合OpenFeign使用Demo
(1)OpenFeign服务调用
(2.1)第一步:创建新的模块,并修改pom
添加依赖:
<dependencies>
<!--表示把这个模块注册到注册中心里去-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入自己定义的API通用包,可以使用Payment支付的Entity-->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
(2.2)第二步:创建配置文件yml
server:
port: 80 #服务端口
spring:
application:
name: cloud-consumer-order #服务名,也就是放进注册中心的名字
eureka:
client:
#表示是否把自己注册到EurekaServer默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true此案呢过配合ribbon使用负载均衡
fetchRegistry: true
#注册中心的地址
service-url:
#defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
(2.3)第三步:创建启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients//激活并启动Feign
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
(2.4)第四步:创建service接口,用来连接8001服务的方法
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
//端口地址对应8001暴露出来的端口地址
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
(2.5)第五步:创建controller类,用来处理请求映射
@RestController
@Slf4j
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}
(2.6)第六步:测试
测试地址:http://localhost/consumer/payment/get/3
(2)OpenFeign超时控制
通过openfeign调用生产者的方法时,会默认等待1秒钟,如果超过1秒没有调用成功,就会报出超时timeout的错误
(3.1)第一步:修改8001服务的controller方法
//测试openfeign的超时控制
//openfeign-ribbon默认等待时间为1秒
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
(3.2)修改80服务的service方法
//测试8001controller中的超时控制方法
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
(3.3)修改80服务的controller方法
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
//openfeign-ribbon,客户端一般默认为等待1秒钟
return paymentFeignService.paymentFeignTimeout();
}
(3.4)测试从8001端口调用timeout方法
测试地址:http://localhost:8001/payment/feign/timeout
访问成功,直接从8001访问的话,不会涉及到openfeign,所以即使耗时3秒也不存在超时的问题
(3.5)测试从80端口调用timeout方法(请求会先到80的controller、然后调用80的service,最后通过openfeign调用8001的controller中的timeout方法)
测试地址:http://localhost/consumer/payment/feign/timeout
如下图,访问失败了,原因就是3秒超时了,这里访问涉及到了openfeign,所以3秒会出现超时的问题
(3.6)修改yml文件,开启openfeign客户端超时控制
添加如下配置:
#openfeign默认等待时间为1秒,又因为openfeign自带Ribbon,所以可以设置客户端超时的时间
ribbon:
#指的是建立连接所用的时间,适用于网络连接所用的时间,设置为5秒
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间,设置为5秒
ConnectTimeout: 5000
重启80服务再测试一下地址:http://localhost/consumer/payment/feign/timeout
【3】OpenFeign日志增强
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。说白了就是对Feign接口的调用情况进行监控和输出
(4.1)编写配置类
日志的打印方式有很多种,我们这里选择最详细的FULL,又因为这里用@Configuration注解声明了,所以待会可以直接在配置文件里修改配置
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
(4.2)修改yml文件
追加如下部分
logging:
level:
#feign日志以什么级别监控哪个接口,以debug形式打印full全日志,这样的日志是最详细的
com.allen.springcloud.service.PaymentFeignService: debug
(4.3)再测试一下
测试地址:localhost/consumer/payment/get/2
看看日志的打印情况
输出的内容包括
- [PaymentFeignService#getPaymentById] —> GET http://CLOUD-PAYMENT-SERVICE/payment/get/2 HTTP/1.1
- [PaymentFeignService#getPaymentById] —> END HTTP (0-byte body)
- [PaymentFeignService#getPaymentById] <— HTTP/1.1 200 (795ms)
- [PaymentFeignService#getPaymentById] connection: keep-alive
- [PaymentFeignService#getPaymentById] content-type: application/json
- [PaymentFeignService#getPaymentById] date: Mon, 13 Apr 2020 11:47:10 GMT
- [PaymentFeignService#getPaymentById] keep-alive: timeout=60
- [PaymentFeignService#getPaymentById] transfer-encoding: chunked
- [PaymentFeignService#getPaymentById]
- [PaymentFeignService#getPaymentById] {“code”:200,“message”:“查询成功,serverPort:8001”,“data”:{“id”:2,“serial”:“alibaba”}}
- [PaymentFeignService#getPaymentById] <— END HTTP (88-byte body)