Feign的基本使用、日志配置和连接池配置详解

一、概述

Feign主要是微服务项目中远程调用的一种实现方式。

常见的远程调用方式有以下几种。

1) RestTemplate

早期的时候远程调用使用的是RestTemplate

String userId = "ZXCVASDD";
// ip和端口在使用注册中心后可以由服务名代替
String url = "http://userservice/findDataByParams/" + userId;
// 发送Http请求 实现不同模块之间的远程调用
User user = restTemplate.getForObject(url, User.class);

这样的编码方式存在很多问题:

  • 代码可读性差,不符合主流编码风格。对于没有接触过远程调用的人来说,非常难以理解。

  • 大量的Http路径维护在代码当中,而且参数复杂url今后难以维护。

因此这样的方式,已经越来越少被使用了。

2) Dubbo

Dubbo是一款优秀的远程RPC框架。

在高并发的情况下,RPC请求会比Http请求性能更加优秀。

@RestController
@RequestMapping("/user")
public class UserController {
    // version 用于灰度发布 代表当前Dubbo服务的版本
    // retries 请求失败时的重试次数
    @DubboReference(version = "2.0.0", retries = 2)
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

Dubbo的编码风格,比较符合主流的编码习惯,对于程序员来说上手不会特别困难。

并且高并发下Dubbo有着不错的性能。

依然是微服务做远程调用的主流选择之一。

3) Feign

Feign是一个声明式的Http客户端,

RestTemplate一样都是用过发送Http请求来完成远程调用。

@Service
public class CardService {
    
    @Autowired
    private UserFeignClient userFeignClient;
    
    public Card feignQueryOrderById(Long cardId) {
        //查询身份证
        Card card = cardMapper.findById(cardId);
        //使用Feign进行远程调用
        User user = (User) userClient.findById(card.getUserId());
        //封装user信息
        card.setUser(user);
        //返回
        return card;
    }
}

但是Feign解决了RestTemplate的所有缺点。

实现了更优雅,可读性更高的代码。

并且Feign内部还集成了负载均衡、日志 、连接池等解决方案、功能非常强大。

基本上已经完全替代了RestTemplate

官方地址:https://github.com/OpenFeign/feign

二、Feign 的使用步骤详解

前言:

假设有两个微服务:

提供者为:UserService

消费者为:CardService

下面就基于这两个微服务演示Feign 的使用。

2.1 提供者端先提供可以被远程调用的接口

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    
   /**
     * 根据id查询用户
     * 路径: /user/110
     * @param id 用户id
     * @return   当前id对应的用户对象
     */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        System.out.println("truth: " + truth);
        return userService.queryById(id);
    }
}

2.2 消费者端引入依赖

消费者端就是需要远程调用其它微服务的那一端。

任何微服务都可能成为消费者端。视具体的业务而定。

<!-- SpringBoot 整合Feign 起步依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.3 消费者端引导类上添加注解@EnableFeignClients

@EnableFeignClients			// 开启Feign的支持
@SpringBootApplication
public class CardApplication {

    public static void main(String[] args) {
        SpringApplication.run(CardApplication.class, args);
    }
}

2.4 当前消费者端编写Feign的客户端

//@FeignClient 声明这是一个Feign客户端
//这个注解的参数就是当前客户端想要远程调用的服务名称

// @FeignClient(value = "userservice" path = "/user")   这么写的话 path的路径就指向具体的controller
//那么这个Client下面的方法,也只能在对应的controller中获取,灵活性会差一些
//所以可以把完整路径写在接口方法签名的上方
@FeignClient(value = "userservice")    
public interface UserClient {

    @GetMapping("/user/{id}")
    Card findById(@PathVariable("id") Long id);

}

微服务名称定义在微服务提供者端的application.yml中的如下位置:

spring:
  application:
    name: userservice             # 服务名称

这里可以发现,Feign 客户端主要基于SpringMVC的注解来声明远程调用的信息。

比较符合主流的编码习惯,学习成本低。

2.5 当前消费者端调用刚才定义的Feign客户端

@Service
public class CardService {
    
    @Autowired
    private UserClient userClient;
    
    public Card feignQueryOrderById(Long cardId) {
        //查询身份证
        Card card = cardMapper.findById(cardId);
        //使用Feign进行远程调用
        User user = (User) userClient.findById(card.getUserId());
        //封装user信息
        card.setUser(user);
        //返回
        return card;
    }
}

这样消费者端的Service层就完成了Feign 的远程调用。

看起来和单体架构的写法没什么区别。

而且代码风格也更加优雅,可读性和可维护性都大大增强。

三、给需要调用的Feign客户端设置异常处理机制

假设调用UserClient中的方法发生异常时,我们也可以自定义异常类去处理。

相当于SpringMVC中的统一异常处理。

3.1 定义异常处理类

异常处理类需要实现FallbackFactory接口,泛型就写定义的Feign客户端的类名

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
                log.error("查询用户异常", throwable);
                return new User();
            }
        };
    }
}

这样在调用UserClient中的服务发生异常时,就会触发create方法。

返回一个各个属性都没赋值的User对象,而不是直接报错让程序终止。

同时日志也会记录下这次异常,方便以后排查。

3.2 在对应微服务的Feign客户端给@FeignClient注解添加参数fallbackFactory

//@FeignClient 声明这是一个Feign客户端
//value 对应的微服务名称
//fallbackFactory 调用UserClient中的方法发生异常时的处理
@FeignClient( = "userservice",  fallbackFactory = UserClientFallbackFactory.class)   
public interface UserClient {

    @GetMapping("/user/{id}")
    Card findById(@PathVariable("id") Long id);

}

四、Feign 的日志配置

Feign支持很多的自定义配置,如下表所示:

类型作用说明
feign.Logger.Level修改日志级别包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder响应结果的解析器http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder请求参数编码将请求参数编码,便于通过http请求发送
feign. Contract支持的注解格式默认是SpringMVC的注解
feign. Retryer失败重试机制请求失败的重试机制,Feign默认是没有实现重试机制

关于重试机制:Feign底层依赖了Ribbon,因此会使用Ribbon的重试,这样Feign就相当于有了重试机制!!

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

平时开发比较常用的还是对日志级别的配置。

所以这里就以日志级别的配置来演示Feign 自定义配置的使用。

4.1 使用application.yml配置文件修改日志级别

4.1.1 针对单个服务配置日志级别
feign:  
  client:
    config: 
      userservice: 			# 针对userservice这个微服务的日志级别
        loggerLevel: FULL 	# 日志级别 
4.1.2 所有微服务配置日志级别
feign:  
  client:
    config: 
      default:		 		# default就是全局配置,如果是写服务名称,则是针对某个具体的微服务的配置
        loggerLevel: FULL 	#  日志级别 

4.2 使用Java代码自定义日志级别

消费者端声明一个类,在类中定义Logger.Level对象

public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

想要使这个类生效,需要给SpringBoot启动类上的@EnableFeignClients添加参数

  • 全局生效:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 
  • 局部生效:
//值针对userservice这个微服务生效
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 

4.3 关于日志的级别的说明

日志级别说明备注
NONE就是没有任何日志记录Feign的默认日志级别
BASIC记录请求的方法,URL以及响应状态码和执行开始和结束时间记录请求基本信息
HEADERS在BASIC的基础上,额外记录了请求和响应的头信息
FULL记录所有请求和响应的明细,包括头信息、请求体、元数据是最全的日志

使用建议:

如果是调试错误,可以使用FULL级别。

平常的话,使用BASIC级别即可。

五、使用Feign连接池

Feign发起Http请求,底层客户端实现通常包括以下几种方式:

实现方式说明
URLConnection默认实现,不支持连接池
Apache HttpClient支持连接池
OKHttp支持连接池

之所以要使用连接池,也是因为每次Http请求,需要三次握手去建立连接,完成后再断开连接。

这在高并发的情况下性能损耗是比较大的。

因此想要提高Feign的性能,可以使用有连接池的底层实现,代替默认的URLConnection

5.1 消费者端引入依赖

<!--Apache HttpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

5.2 配置Feign连接池

feign:
  client:
    config:
      default: 						# default		全的配置
        loggerLevel: BASIC 			# 日志级别配置	BASIC基本的请求和响应信息
  httpclient:
    enabled: true			 		# 开启feign对HttpClient的支持
    max-connections: 100			# 连接池的 最大的连接数
    max-connections-per-route: 50 	# 连接池的 每个路径的最大连接数

六、分模块开发抽取Feign模块

上面Feign的使用步骤中对Feign的使用方案,大部分新手都会这么去使用Feign

但是这样会引发一个设计层面问题:

假设一个消费者端要使用提供者UserService中提供的而服务。那么就要写一次UserClient.

那么在多个不同的消费者端都要使用提供者UserService中提供的而服务时,UserClient会写非常多次。

这非常不利于将来的维护,而且代码也过于冗余。

解决方案:

利用Maven分模块开发抽取Feign模块。

这样只需要把每个服务的Client,都只在单独的Feign模块中写一次。

并且把接口有关的POJO、默认的Feign配置都放到这个模块中。

各个消费者端再引入这个单独的Feign模块就可以了。

具体操作:

6.1 单独创建一个module,命名为feign-api

6.2 给feign-api模块引入feign客户端的起步依赖

<!-- feign客户端的依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

6.3 在所有需要远程调用其它服务的消费者端引入feign-api模块的对应依赖

<!-- 这里的groupId 视自己创建模块时设置的groupId即可 -->
<dependency>
    <groupId>com.feign.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

6.4 配置包扫描

之所以要配置包扫描,是因为消费者端的包扫描路径是在SpringBoot启动类的包及其子包下。

这个路径和feign-api模块中写的各个服务的Feign客户端不一定一致。

如果两个路径不一致,就会导致消费者端的Spring容器中没有对应服务的Feign客户端对象。

导致依赖注入失败。

方式一:配置具体需要引入的Feign客户端

在引入feign-api的消费者端的SpringBoot的引导类上配置注解

@EnableFeignClients(clients = {UserClient.class})

注解参数中指定了具体需要引入哪个提供者服务的UserClient

clients参数是一个数组,可以传递多个值。

@EnableFeignClients(clients = {UserClient.class})
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

方式二:直接配置Feign的包扫描路径

在引入feign-api的消费者端的SpringBoot的引导类上配置注解

@EnableFeignClients(basePackages = "com.feign.clients")

@EnableFeignClients(basePackages = "com.feign.clients")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐