Declarative HTTP client - Spring Cloud OpenFeign

Feign

What is Feign:

Feign consists of:
Declarative HTTP client - Spring Cloud OpenFeign

  • Feign.Builder: All FeignClient are constructed from Feign.Builder
  • Client: Internal feign.Client.Default actually use the HttpURLConnection, and LoadBalanceFeignClient default incoming case is feign.Client.Default, just added a load balancing feature. Compared to feign.Client.Default, LoadBalanceFeignClient support incoming specified Client
  • Contract: native Feign is not supported @ GetMapping, @ PostMapping ... SpringMVC such as annotations, Spring Cloud support for its extension before

Feign specified log level

Feign default is not to print any of the log, but the interface is called the actual project problems need to debug code or time-consuming need to see an interface calls executed, the first time think of to see Feign log, this time to how Feign log to open it? There are two main ways, through code configuration or through configuration files. In addition, the configuration takes effect range also divided into local configuration and global configuration, we first introduce the local fine-grained configuration.

Note that, Feign Spring Boot logging levels and not the same, so you can not directly configure Spring Boot log level to open. Feign log level in the following table:
Declarative HTTP client - Spring Cloud OpenFeign

1, local configuration - code configuration; through code configured with two main steps, the first corresponding configuration type defined in the code, and then to the interface configuration file is FeignClient log level. First, the definition of Feign log level configuration class. code show as below:

package com.zj.node.contentcenter.configuration;

import feign.Logger;
import org.springframework.context.annotation.Bean;

/**
 * @author 01
 * @date 2019-07-29
 **/
public class UserCenterFeignConfig {

    @Bean
    public Logger.Level level(){
        // 设置Feign的日志级别为FULL
        return Logger.Level.FULL;
    }
}

注:该类不要加上@Configuration注解,否则将会因为父子上下文扫描重叠而成为全局配置

由于不是做的全局配置,所以除此之外还需要在FeignClient接口中指定该配置类:

package com.zj.node.contentcenter.feignclient;

import com.zj.node.contentcenter.configuration.UserCenterFeignConfig;
import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "user-center", configuration = UserCenterFeignConfig.class)
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

然后在配置文件中添加如下配置:

# 设置日志级别
logging:
  level:
    # 这里需要配置为debug,否则feign的日志级别配置不会生效
    com.zj.node.contentcenter.feignclient.UserCenterFeignClient: debug

配置完成后,启动项目执行相应的调用代码,控制台输出的日志如下:
Declarative HTTP client - Spring Cloud OpenFeign


2、局部配置 - 配置文件配置;这种配置方式就比较简单,也是比较常用的方式,只需在配置文件中添加如下配置即可:

# 定义feign相关配置
feign:
  client:
    config:
      # 微服务名称
      user-center:
        # 设置feign日志级别
        loggerLevel: full

# 设置日志级别
logging:
  level:
    # 这里需要配置为debug,否则feign的日志级别配置不会生效
    com.zj.node.contentcenter.feignclient.UserCenterFeignClient: debug

1、全局配置 - 代码配置;同样定义一个配置类:

public class GlobalFeignLoggerConfig {

    @Bean
    public Logger.Level level(){
        // 设置Feign的日志级别为FULL
        return Logger.Level.FULL;
    }
}

然后配置启动类上的@EnableFeignClients注解的defaultConfiguration属性,如下:

@EnableFeignClients(
        basePackages = "com.zj.node.contentcenter.feignclient",
        defaultConfiguration = GlobalFeignLoggerConfig.class
)

接着将配置文件中的日志配置从特定的类修改为包名,如下:

# 设置日志级别
logging:
  level:
    # 这里需要配置为debug,否则feign的日志级别配置不会生效
    com.zj.node.contentcenter.feignclient: debug

2、全局配置 - 配置文件配置;

# 定义feign相关配置
feign:
  client:
    config:
      # default表示为全局配置
      default:
        # 设置feign日志级别
        loggerLevel: full

# 设置日志级别
logging:
  level:
    # 这里需要配置为debug,否则feign的日志级别配置不会生效
    com.zj.node.contentcenter.feignclient: debug

Feign支持的配置项

由于使用代码方式配置和使用配置文件配置所支持的配置项不同,所以分为两类。

1、代码方式所支持的配置项:

配置项 作用
Feign.Builder Feign的入口
Client Feign底层用什么http客户端去请求
Contract 契约,注解支持
Encoder 编码器,用于将对象转换成Http请求消息体
Decoder ×××,将响应消息体转换成对象
Logger 日志管理器
Logger.Level 指定日志级别
Retryer 指定重试策略
ErrorDecoder 指定异常×××
Request.Options 超时时间
Collection<RequestInterceptor> 请求拦截器
SetterFactory 用于设置Hystrix的配置属性,Feign整合Hystrix才会用

2、配置文件所支持的配置项:
Declarative HTTP client - Spring Cloud OpenFeign

代码配置 vs 配置文件配置:
Declarative HTTP client - Spring Cloud OpenFeign

  • 关于优先级:细粒度配置文件配置 > 细粒度代码配置 > 全局配置文件配置 > 全局代码配置

配置最佳实践总结:

  • 尽量使用配置文件配置,配置文件满足不了需求的情况下再考虑使用代码配置
  • 在同一个微服务内尽量保持单一性,例如统一使用配置文件配置,尽量不要两种方式混用,以免增加定位问题的复杂度

Feign的继承

所谓Feign的继承实际是为了服务之间能够复用代码,例如现在用户中心服务有一个按id查询用户信息的接口如下:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }
}

若我想在内容中心服务通过Feign调用该接口,就需要新建一个interface,并编写如下代码:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

可以看到,方法的定义实际上是一样的,所以这时候就可以利用Feign的继承特性复用这种代码。首先需要创建一个单独的项目或maven模块,因为这样才能通过添加maven依赖的方式引入到不同的项目中。这里暂且称为api模块吧,在api模块中定义一个这样的接口,代码如下:

@RequestMapping("/users")
public interface UserApi {

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

然后在用户中心服务中添加api模块的依赖,接着实现UserApi接口,改写之前的UserController如下:

@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController implements UserApi {

    private final UserService userService;

    @Override
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }
}

在内容中心服务中也添加api模块的依赖,改写之前的UserCenterFeignClient代码,让其继承UserApi,代码如下:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient extends UserApi {
}

可以看到,继承了UserApi后,此时不需要再定义与目标接口相同的方法了,复用了上级接口的代码,这就是所谓Feign的继承。

其实关于这种使用方式存在许多争议,我们来看看官方怎么说:

It is generally not advisable to share an interface between a server and a client. It introduces tight coupling, and also actually doesn’t work with Spring MVC in its current form (method parameter mapping is not inherited).

大致翻译如下:

通常不建议在服务提供者(server)和服务消费者(client)之间共享接口,因为这种方式引入了紧耦合,并且实际上在当前形式下也不适用于Spring MVC(方法参数映射不会被继承)

  • 关于方法参数映射不会被继承:在上面的代码示例中可以看到,实现UserApi的UserController方法参数上,依旧需要写MVC相关的注解,因为这些注解是不会被继承的。简单来说就是这类注解得写在实现类的方法参数上才会生效,而对于团队中对此不甚熟悉的开发人员来说也会造成一定的”迷惑“

官网文档地址如下:

https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#spring-cloud-feign-inheritance

关于继承特性的争议:

  • 官方观点:不建议使用
    • 理由上面已说明
  • 业界观点:很多公司使用
    • 理由1:代码可复用;面向契约
    • 理由2:在业务需求变更比较频繁的情况,无需修改太多的代码

如何抉择:

根据项目情况权衡利弊即可,若需要这种特性带来的好处又可以承受紧耦合带来的负面影响,那么就选择使用该特性,否则就不要使用


Feign发送多参数GET请求的坑

使用过Spring MVC的都知道,当一个GET接口有多个请求参数时可以使用对象来接收。例如用户服务中,有这样一个接口如下:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/query")
    public User query(User user) {
        return user;
    }
}

使用postman发送如下请求是可以正常接收并响应的:
Declarative HTTP client - Spring Cloud OpenFeign

所以在另一个服务中使用Feign调用这种类型的接口时,我们很自然而然的就会写成如下形式:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/query")
    UserDTO query(UserDTO userDTO);
}

实际上这种使用Feign发送多参数GET请求的方式是会有坑的,因为将多参数包装成对象时,Feign在底层会将其转换为POST请求,并把对象序列化塞到http body中,所以就会由于不支持该请求方法而报405错误。

关于这个坑我们做个实验来验证一下,在内容中心服务中,定义一个接口如下:

@RestController
@RequestMapping("/shares")
@RequiredArgsConstructor
public class ShareController {

    private final UserCenterFeignClient userCenterFeignClient;

    @GetMapping("/queryUser")
    public UserDTO queryUser(UserDTO userDTO){
        return userCenterFeignClient.query(userDTO);
    }
}

然后通过postman进行请求,可以看到直接报405错误了:
Declarative HTTP client - Spring Cloud OpenFeign

此时用户服务的控制台中,输出了如下日志信息:

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

那么我们要如何去解决这个坑呢?最显而易见的方式就是不将参数包装成对象,而是拆解开来使用@RequestParam一个个写上去。然而这种方式有个很明显的弊端,如果有很多参数的时候,一个个写就比较累,而且代码也不好看。在这种“走投无路”的情况下,就会想着要不就不用GET了,换成POST吧。虽然这种方法也可行,但是却违背了RESTful的规范。

那有没有一个完美的解决方案呢?答案是有的,那就是使用@SpringQueryMap注解,该注解相当于feign.QueryMap,目的是将对象转换为GET参数。那么我们就来试试看吧,修改UserCenterFeignClient代码如下:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/query")
    UserDTO query(@SpringQueryMap UserDTO userDTO);
}

注:该注解在spring-cloud-starter-openfeign: 2.1.0及之后的版本才开始支持的,之前的版本只能使用其他方式解决该问题。之所以会有这个坑,也是因为原生Feign的体系让Spring Cloud无法封装得与Spring MVC完全一致的编程体验

修改完代码后重启项目,再次使用postman请求就没有报错了:
Declarative HTTP client - Spring Cloud OpenFeign


Feign脱离Ribbon使用

我们都知道Feign内部整合了Ribbon,所以才能有负载均衡功能及从服务发现组件获取服务实例的调用地址功能。那么如果需要调用一个没有注册到服务发现组件上的服务或地址,即脱离Ribbon去使用Feign的话,要如何做呢?非常简单,只需要配置一下@FeignClient注解的url属性即可。如下示例:

// name是必须配置的,否则项目都无法启动,url属性通常是配置basic地址
@FeignClient(name = "baidu", url = "https://www.baidu.com")
public interface TestFeignClient {

    @GetMapping
    String index();
}

然后定义一个接口测试一下:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final TestFeignClient feignClient;

    @GetMapping("/baidu")
    public String baiduIndex() {
        return feignClient.index();
    }
}

启动项目,浏览器访问如下:
Declarative HTTP client - Spring Cloud OpenFeign


Feign性能优化

RestTemplate VS Feign:
Declarative HTTP client - Spring Cloud OpenFeign

从上图中可以看到,Feign只在性能和灵活性上输给了RestTemplate,至于灵活性官方也说了无论如何优化也不可能像RestTemplate一样,而性能则是可以进一步提高的。

Feign default in the performance of about 50% RestTemplate, although the bottleneck of the project generally does not appear on the Feign, but if we make a better case Feign the performance of some, it is only for the good, so this section briefly about the Feign performance optimization.

By default, the bottom layer is used Feign HttpURLConnection request is sent, there is no known HttpURLConnection connection pool to use, it can be optimized for this. For example, the bottom of the http request of the client to replace the use of http client connection pool for the Apache HttpClient OkHttp or the like, according to the connection pool tests used to enhance the performance of about 15%.

Another optimization point is to set a reasonable level of logging, Introduced before the log level configuration, so just to demonstrate how to replace the other clients http request and configure the connection pool Feign.

First to HttpClient here, for example, the first step increase dependence:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

2. Add configuration:

feign:
  httpclient:
    # 让feign启用httpclient作为发送http请求的客户端
    enabled: true
    # 最大连接数
    max-connections: 200
    # 单个路径的最大连接数
    max-connections-per-route: 50    

Use okhttp is the same, plus the first step dependence:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

2. Add configuration:

feign:
  okhttp:
    # 让feign启用okhttp作为发送http请求的客户端
    enabled: true
    # 最大连接数
    max-connections: 200
    # 单个路径的最大连接数
    max-connections-per-route: 50

Welcome to my personal public concern number:

  • Program ape hodgepodge
  • From time to time share original short technical articles or interesting IT News
  • You can also talk about the vicissitudes of life
    Declarative HTTP client - Spring Cloud OpenFeign

Guess you like

Origin blog.51cto.com/zero01/2424667