从零开始 Spring Cloud 6:Feign

从零开始 Spring Cloud 6:Feign

从零开始 Spring Cloud 6:Feign_第1张图片

图源:laiketui.com

虽然就像在本系列第一篇文章中演示的那样,我们可以用RestTemplate对接口进行远程调用,并且可以实现负载均衡。但是这样存在一些问题,比如需要手动指定 URL 地址,以及显式实现接口返回 JSON 数据的解码。

实际上 Spring Cloud 框架中首选的 Http 调用客户端是 Feign,使用它可以简化很多工作。

下面我们看如何使用 Feign。

基础

依赖

在子模块 shopping-order 中添加依赖:

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-openfeignartifactId>
dependency>

@EnableFeignClients

在配置类上添加@EnableFeignClients开启相关功能:

@EnableFeignClients
@MapperScan("org.example.shopping.order.mapper")
@SpringBootApplication
public class OrderApplication {
	// ...
}

@FeignClient

添加一个接口UserClient用于远程调用:

@FeignClient("shopping-user")
public interface UserClient {
    @GetMapping("/user/{id}")
    Result<User> getUserInfo(@PathVariable Long id);
}

@FeignClient注解的value属性是接口对应的服务名,即在 Nacos 上注册的服务名。

@GetMapping注解和@PathVariable注解都是 Spring MVC 的注解,其用途也是类似的。在这里,@GetMapping注解说明这个远程调用使用的是 Http GET 方法,@PathVariable映射的是@GetMapping中的路径参数{id}

接口方法getUserInfo的返回类型Result说明了远程调用返回的类型。

上面这个示例,实际上对应了对接口http://shopping-user/user/{id}的调用。

调用

使用 Feign 远程调用很容易,像使用本地服务一样调用即可:

@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class OrderService {
	// ...
    @Autowired
    private UserClient userClient;

    public Order findOrderById(Long orderId) {
        Order order = orderMapper.findById(orderId);
        Result<User> userInfo = userClient.getUserInfo(order.getUserId());
        order.setUser(userInfo.getData());
        return order;
    }
}

测试

重启子模块 shopping-order 后访问接口 http://localhost:8080/order/101,可以正常返回数据,并且观察控制台可以看到,多个 shopping-user 实例都会接收到请求,所以负载均衡依然是生效的。

与 RestTemplate 的对比

RestTemplate 相比,Feign 是声明式的方式进行调用,更容易理解和调用。并且无需使用@LoadBalanced注解,默认使用负载均衡方式进行调用。此外,如果返回类型包含泛型参数,Feign 也可以很好地处理 JSON 解码。

自定义配置

Feign可以支持多种自定义配置,如下表所示:

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

Feign 的日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

可以通过配置文件或代码方式对配置进行修改,下面以修改日志级别为例进行说明。

配置文件方式

修改全局日志级别:

feign:
  client:
    config:
      default:
        loggerLevel: BASIC

修改指定服务的日志级别:

feign:
  client:
    config:
      default:
      	shopping-user:
          loggerLevel: FULL

代码方式

添加一个 Logger.Level 类型的 bean 定义:

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level loggerLevel(){
        return Logger.Level.BASIC;
    }
}

修改全局日志级别:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
// ...
public class OrderApplication {
    // ...
}

修改具体 FeignClient 的日志级别:

@FeignClient(value = "shopping-user", configuration = DefaultFeignConfiguration.class)
public interface UserClient {
    @GetMapping("/user/{id}")
    Result<User> getUserInfo(@PathVariable Long id);
}

性能优化

Feign 的底层 Http 调用并不是自己实现的,而是使用现成的 Http 客户端:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

默认使用 JDK 自带的 URLConnection 实现 Http 调用,这个 Http 客户端不支持连接池,所以我们可以改用另外两种 Http 客户端进行替换,来改善性能。

下面使用 HttpClient 进行替换。

依赖

在子模块 shopping-order 中添加 HttpClient 的依赖:


<dependency>
    <groupId>io.github.openfeigngroupId>
    <artifactId>feign-httpclientartifactId>
dependency>

配置连接池

在配置文件中添加相关连接池配置信息:

feign:
  httpclient:
    enabled: true
    max-connections: 200
    max-connections-per-route: 50

除了将底层 Http 客户端修改为 HttpClientOKHttp 以外,还应当在生产或测试环境使用 BASICNONE 级别的日志,打印更多的日志信息同样会影响性能。

最佳实践

如果我们的项目只是一个小型项目,直接在子模块中创建 FeignClient 并没有什么问题,但如果是一个大型项目,在每个子模块中为同一个服务的远程调用创建 FeignClient 就显得很低效了,这没有体现重用的思想。

抽取方式

可以为微服务所提供的接口专门创建一个子模块,用于保存对应的 FeignClient,这样其他子模块需要调用接口时,只要直接从这个现成的子模块引用并使用 FeignClient 即可,不需要自行创建。

下面我们基于这个思路来改造当前项目。

改造前的项目代码对应的仓库是:ch6/shopping,改造后的项目仓库是ch6/shopping2。

先添加一个子模块 feign-api,用于存放 FeignClient

  • 添加子模块的方式可以参考这篇文章。
  • 如果子模块的 pom.xml 文件是灰色带删除线的,可能是因为之前建立并删除过该模块,idea 会将该模块的 pom.xml 文件设置为被忽略的文件,此时应该在 file->settings->Maven->Ignored Files 设置中将对应的 pom.xml 文件取消忽略。

src/main/java 中添加包 org.example.shopping.feignapi 作为子模块的根包。

shopping-order 中的 FeignClient 接口移动到 feign-api

从零开始 Spring Cloud 6:Feign_第2张图片

因为缺少依赖和其它类的引用,所以这里都是红色的。

添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
dependencies>

还需要将实体类User移动到 feign-api

package org.example.shopping.feignapi.entity;
// ...
@Data
public class User {
    private Long id;
    private String userName;
    private String address;
}

类似的,还要移动作为统一接口返回类型的Result类和配置类DefaultFeignConfiguration,这里不再赘述。

shopping-order 中删除已经以移动到 feign-api 模块中的类。

shopping-orderpom.xml 中添加对 feign-api 模块的依赖:

<dependency>
    <groupId>org.examplegroupId>
    <artifactId>feign-apiartifactId>
    <version>1.0version>
dependency>

现在就可以在 shopping-order 中引用 feign-api 中的类了。之后只要处理出错的类引用,让其引用 feign-api 中的相应类即可。

现在启动子模块 shopping-order 会报错:

Consider defining a bean of type 'org.example.shopping.feignapi.client.UserClient' in your configuration.

这是因为默认情况下,使用@EnableFeignClients注解可以扫描子模块包下的@FeignClient标记的接口,并创建相应的 bean 用于接口调用。但现在 shopping-order 使用的是 feign-api 下的 FeignClient,而其包名是org.example.shopping.feignapi.client,并非org.example.shopping.order或其子包。所以自动扫描是扫描不到的,自然不会创建对应的 bean。

要让目标 FeignClient 能够被扫描到,需要:

@EnableFeignClients(basePackageClasses = UserClient.class)

或者:

@EnableFeignClients(basePackages = "org.example.shopping.feignapi.client")

后者会扫描并创建org.example.shopping.feignapi.client包下的所有 FeignClient

如果配置了 Feign 日志级别,但是没有输出日志信息,可以检查配置中是否有将 FeignClient 所在的包的日志级别设置为 DEBUG

logging:
  level:
    org.example.shopping.feignapi: debug

上边介绍的方式主要是将各个子模块中的 FeignClient 抽取并集中到一个单独的 feign-api 模块中进行管理和引用。除了这种方式以外,还有一种用继承的方式进行约束和重用的方法。

继承方式

改造前的代码仓库为ch6/shopping,改造后的代码仓库为ch6/shopping3。

shopping-user 子模块中定义 API 接口:

package org.example.shopping.user.feignapi;
// ...
public interface UserAPI {
    @GetMapping("/user/{id}")
    Result<User> getUserInfo(@Min(1) @NotNull @PathVariable Long id);
}

这个接口约束了接口地址、参数校验、参数类型等。

shopping-userController 可以实现这个接口:

@RestController
@RequestMapping("")
@Validated
public class UserController implements UserAPI {
    @Override
    public Result<User> getUserInfo(Long id) {
        return Result.success(userService.findUserById(id));
    }
    // ...
}

可以注意到因为接口方法有@GetMapping注解定义路径,并且有 Hibernate Validation 的相关注解进行校验,所以这里不需要重复使用相应的注解。

在作为调用方的 shopping-order 模块中,需要添加对 shopping-user 模块的依赖:

<dependency>
    <groupId>org.examplegroupId>
    <artifactId>shopping-userartifactId>
    <version>1.0version>
dependency>

现在 shopping-order 中的 FeignClient 只要简单继承 shopping-user 中的接口即可:

@FeignClient(value = "shopping-user")
public interface UserClient extends UserAPI {
}

这种方式的好处在于结构简单,并没有使用单独的模块管理 FeignClient,并且一定程度上简化并重用了代码。但缺点在于将接口的提供方和接口的调用方进行了紧耦合,并不利于代码扩展。

The End,谢谢阅读。

本文的完整示例代码可以从这里获取。

你可能感兴趣的:(JAVA,spring,cloud,feign)