尽管使用了注册中心来解决URL的硬编码等问题,但是如今使用RestTemplate还是存在以下问题:

  • 代码可读性差,编程体验不统一;
  • 参数复杂URL难以维护。

Feign是一个声明式的HTTP客户端,作用于服务消费者,在服务消费者中为服务提供者创建一个HTTP远程调用。官方地址:https://github.com/OpenFeign/feign。其作用就是帮助我们优雅的实现HTTP请求的发送,解决上面提到的问题。

使用 Feign

使用Feign非常简单,大致分为以下步骤:

  1. pom.xml中引入Feign客户端依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  2. 在项目的启动类上添加注解@EnableFeignClients以开启Feign的功能。例如为order-service(服务消费者)开启Feign:

    @EnableFeignClients
    @MapperScan("asia.linner.demo.order.mapper")
    @SpringBootApplication
    public class OrderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderApplication.class, args);
        }
    }
    
  3. 为服务提供者编写一个FeignClient接口。例如在order-service(消费者)中为user-service(提供者)编写FeignClient

    @FeignClient("user-service")
    public interface UserClient {
    
        @GetMapping("/user/{id}")
        User findById(@PathVariable Long id);
    }
    
    • @FeignClient:标注该接口为FeignClient,其value属性指定一个客户端的服务名称。
    • @GetMapping:为了方便使用,Feign使用的是Spring的注解,其用法和作用与Spring中的类似。

    FeignClient可以放在项目中的clients包下。

  4. 通过FeignClient远程调用服务。例如在order-service(消费者)中通过FeignClient远程调用user-service(提供者):

    @Service
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
    
        // 注入Feign客户端
        @Autowired
        private UserClient userClient;
    
        public Order queryOrderById(Long orderId) {
            // 1.查询订单
            Order order = orderMapper.findById(orderId);
            // 2.利用Feign远程调用
            User user = userClient.findById(order.getUserId());
            // 3.封装User到Order
            order.setUser(user);
            // 4.返回
            return order;
        }
    }
    

注意:使用了FeignClient,原本声明RestTemplateBean可以删除掉。因为使用FeignClient并不需要RestTemplateBean


FeignClient 配置

Feign可以修改的配置如下:

类型 作用 说明
feign.Logger.Level 修改日志级别 Feign包含四种不同的日志级别:
  • NONE:不记录任何日志。
  • BASIC:基础日志级别。记录请求方法、URL以及响应状态代码和执行时间。
  • HEADERS:记录基本信息以及请求和响应头信息。
  • FULL:全日志级别。记录基本信息以及请求和响应头信息、请求和响应体信息。
feign.codec.Decoder 响应结果的解析器 HTTP远程调用的结果做解析,例如解析JSON字符串为Java对象。
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过HTTP请求发送。
feign.Contract 支持的注解格式 默认是SpringMVC的注解。
feign.Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试。

一般需要配置的是日志级别。有以下几种配置方式:

application.yml中对全局进行配置:

feign:          # Feign配置
  client:       # 客户端配置
    config:
      default:  # 默认配置(全局生效)
        logger-level: FULL  # 日志级别

application.yml中对指定的服务进行配置。例如在order-service中对user-service进行配置:

feign:                # Feign配置
  client:             # 客户端配置
    config:
      user-service:   # 指定服务进行配置
        logger-level: HEADERS   # 日志级别

只需要将全局默认配置中的default改成指定的服务名称即可。

另外一种方法是创建FeignClient配置类:

import feign.Logger;

/**
 * FeignClient配置类
 */
public class DefaultFeignClientConfig {
    @Bean
    public Logger.Level getFeignLogLevel() {
        return Logger.Level.BASIC;
    }
}

注意:FeignClient配置类中的Logger导入的是feign包下的Logger。并且在application.yml中的配置需要注释掉,否则即使开启了配置,配置类中的配置也不会生效。因为application.yml中的配置会将配置类中的配置覆盖掉。

创建好了FeignClient配置类,这些配置并不会生效。因为FeignClient配置类中并没有任何信息告诉Spring这个是个配置类。所以需要对配置类进行声明。

有两种声明方式,一种是在项目的启动类中进行声明,告诉Spring这个是FeignClient的配置类。并且这种声明方式会在全局生效。声明FeignClient的配置类需要在启动类中使用@EnableFeignClients注解,并为其defaultConfiguration属性指定该FeignClient的配置类的class。例如为order-service声明该配置类:

@EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class)  // 全局默认的Feign配置
@MapperScan("asia.linner.demo.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

另外一种方式是,在某个具体服务FeignClient的接口中进行声明。该方式使用@FeignClient注解,并为其configuration属性指定一个FeignClient配置类的class。例如在order-service中为user-service声明使用一个UserFeignClientConfig配置类(假设已经创建好了该配置类):

@FeignClient(value = "user-service", configuration = UserFeignClientConfig.class)
public interface UserClient {

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

记录日志会损耗一部分性能,所以除了在开发过程中使用FULL日志级别。在生产环境中最好使用BASICNONE日志级别以减少性能损耗(尽量使用BASIC)。


配置连接池

每次HTTP请求,都需要三次握手去建立连接,完成后再断开连接。在高并发的情况下,这样往复地操作会造成的性能损耗是比较大的。引入连接池是为了减少这种性能的损耗。

Feign底层发起HTTP请求,依赖于其它的框架。其底层客户端实现包括:

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

提高Feign的性能主要手段就是使用HttpClient或OKHttp连接池代替默认的URLConnection。

这里选择使用HttpClient。首先在消费者中引入其依赖:

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

然后在application.yml中修改配置:

feign:                # Feign配置
  httpclient: # HttpClient配置
              # 如果要使用OKHttp,在feign.okhttp中做相应的配置即可
    enabled: true # 支持HttpClient的开关
                  # 默认是true,但是没引入依赖不会生效
    max-connections: 200  # 最大连接数
    max-connections-per-route: 50 # 单个请求路径的最大连接数

提高Feign的性能还可以对连接池客户端的最大连接数根据实际情况进行相应的配置调整。


抽取API接口

由于FeignClient接口中编写的接口方法与其对应的提供者中的Controller的方法一致。所以可以对FeignClient接口和Controller做一个统一的API接口抽取,然后再通过集成的方式分别去实现FeignClient和Controller。但是这样的方法有以下缺点:

  • 服务提供方、服务消费方紧耦合。

  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解。

    在API父接口的方法参数中使用的注解不会对Spring(也就是Controller)生效。即@PathVariable@RequestParam这样的注解,在API父接口中声明了,在对应的Controller中也需要再次声明。

user-service为例:

  • API父接口:

    public interface UserAPI {
        @GetMapping("/user/{id}")
        User findById(@PathVariable Long id);
    }
    
  • FeignClient

    @FeignClient("user-service")
    public interface UserClient {}
    
  • Controller:

    @RestController
    public interface UserAPI {
        User findById(@PathVariable Long id) {
            /* 业务代码... */
        }
    }
    

这种方法的优点是简单、实现了代码共享,遵循了面向契约的编程思想。


抽取 feign-api 模块

另外一种方式是将所有的提供者对应的FeignClient抽取为独立的模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,通过Maven引入依赖的方式提供给所有消费者使用。

假设有多个消费者都需要调用到同一个提供者。如果让消费者分别实现自己的FeignClient,不仅会有许多冗余的代码,而且也不利于维护。但是如果使用这种方式将FeignClient抽取出来,可以由实现提供者的程序员来提供对应的feign-api实现。

这样的方法也有一些缺点,在使用一个提供者的接口时,需要同时引入该提供者的所有接口和其它提供者的所有接口。

抽取feign-api的步骤:

  1. 创建一个新的模块,命名为feign-api

  2. feign-api中引入Feign的Stater依赖:

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

    如果要默认使用HttpClient连接池,还需要导入其坐标:

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

    注:引入坐标是为了在消费者的pom.xml中可以不同显式地导入HttpClient的依赖(使用Maven的依赖传递),但是HttpClient的配置还是得在消费者中的application.yml中配置。

    如果在feign-api中创建application.yml中并配置HttpClient,这样的配置是不会在消费者中生效的。因为feign-api没有启动类,而且消费者的启动类也不会使用feign-apiapplication.yml的配置。除非使用配置类编写对HttpClient的配置,并且在消费者中启用这个配置类。

  3. 将所有消费者的FeignClient、POJO和DefaultFeignClientConfig都抽取到feign-api模块中。

    注:DefaultFeignClientConfig的抽取是对所有的FeignClient做一个默认的配置抽取。

  4. 在消费者中引入feign-api依赖。

  5. 在消费者中使用feign-api提供的API接口。

    注:需要修改消费者的启动类,在消费者的启动类上使用@EnableFeignClients注解的basePackagesclients属性指定扫描的FeignClient包或具体的FeignClient类,让消费者的启动类能扫描到feign-apiFeignClient

order-service(消费者)和user-service(提供者)为例:

  1. 创建feign-api,并导入Feign依赖。

  2. 将原本编写在order-service中的UserClientUserDefaultFeignClientConfig抽取出来,放在feign-api中对应的包下。例如:

    asia.linner.demo.feignfeign-api的包名)下的包结构:

    • clients
      • UserClient.java
    • pojo
      • User.java
    • config
      • DefaultFeignClientConfig.java

    抽取完成后,原本在order-service中的UserClientUserDefaultFeignClientConfig都可以删除。但是需要注意复制在feign-api中的UserClientUserDefaultFeignClientConfig它们的包名要改成feign-api的包名。

  3. order-service中导入feign-api

    <!--引入抽取的feign-api模块-->
    <dependency>
        <groupId>asia.linner.demo</groupId>
        <artifactId>feign-api</artifactId>
        <version>1.0</version>
    </dependency>
    

    order-service中的Feign依赖可以删除;如果有在feign-api中导入并配置HttpClient,HttpClient的依赖也可以删除。需要注意引入order-service中的UserClientUserDefaultFeignClientConfig它们的包名要改成feign-api的包名。

  4. order-service的启动类扫描FeignClient

    因为feign-apiorder-service的包名并不相同(如asia.linner.demo.feignasia.linner.demo.order),所以在没有扫描包指定的情况下order-service的启动类并不能扫描到feign-api中的UserClient,所以会导致order-service中的UserClient注入失败。

    Feign的@EnableFeignClients注解提供了两种方式来让消费者的启动类扫描到FeignClient

    • basePackages

      @EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class,
          basePackages = "asia.linner.demo.feign.clients" // 扫描整个clients包
      )
      
    • clients

      @EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class,
          clients = {UserClient.class}    // 指定需要加载的FeignClient接口
      )
      

      clients属性的类型是一个class数组,所以可以指定多个FeignClient。推荐使用该方式。

    在上述方法中选一种,然后修改order-service的启动类即可。