openFeign让http调用更简单

2023年 8月 13日 103.3k 0

1 一个简单的例子

openfeign提供了一种声明式的http调用方式,用户只需要提供http的请求参数和接收响应的数据类型即可。同时,openfeign的http接口定义方式和使用的注解与spring mvc定义服务端接口的时候一模一样,只是不需要提供接口具体的实现,大大方便了用户的使用,对spring开发者来说没有任何门槛。

要使用openfeign调用http接口,只需要在pom.xml文件中添加依赖:

    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    

在spring配置类上添加@EnableFeignClients注解:

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class AppClient1Application {

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

}

然后定义一个添加了@FeignClient注解的接口,如下所示,通过@FeignClientname参数(即Feign client的名字)指定服务名,通过@PostMapping@PathVariable@RequestParam@RequestBody等注解指定请求参数:

@FeignClient(name = "app-server")
public interface AppServerFeign {

    @PostMapping(value = "/hello/{who}", headers = {"Content-Type: application/json"})
    HelloResponse hello(@PathVariable String who, @RequestParam String type, @RequestBody HelloRequest request);
}

public class HelloRequest {
    private String content;
}

public class HelloResponse {
    private String who;
    private String type;
    private String content;
}

然后就可以调用接口了:

@RestController
@RequestMapping(value = "/")
public class Controller {

    private final AppServerFeign appServerFeign;

    public Controller(AppServerFeign appServerFeign) {
        this.appServerFeign = appServerFeign;
    }

    @GetMapping(value = "/hello")
    public HelloResponse hello() {
        return appServerFeign.hello("movee", "email", new HelloRequest("hello world"));
    }

}

在调用feign client接口的方法时,feign client会从注册中心获取服务的实例,然后构造http请求,并将响应解析为返回类型对象。

2 打开日志调试开关

调试中,我们经常想查看http的调用情况,可以通过以下方式打印openfeign的请求日志(注意:spring boot3的openfeign配置放在spring.cloud.openfeign配置空间下了,不再放在feign配置空间下,如果从spring boot2迁移过来,原先配置在feign空间下的配置会不生效):

logging:
  level:
    root: INFO
    # 包的日志级别设置为DEBUG
    movee.app.client1.io.AppServerFeign: DEBUG

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            # openfeign的默认日志等级
            loggerLevel: FULL

也可以单独控制特定的feign接口的日志等级

spring:
  cloud:
    openfeign:
      client:
        config:
          # openfeign client的默认配置
          default:
            # 默认不打印
            loggerLevel: NONE
          # 特定openfeign client的配置
          app-server:
            loggerLevel: FULL

openfeign共定义了四个loggerLevel

NONE: 不打印
BASIC: 打印请求方法、url、响应状态等基本信息
HEADERS:除了打印BASIC等级的基本信息外,还打印请求和响应头
FULL:打印比较详细的请求和响应信息,包括请求和响应body的具体内容

3 @FeignClient注解的常用配置
3.1 直接指定服务的地址

上面的例子,是指定服务的名字,然后openfeign通过从注册中心获取实例后选择调用的地址。但是,有些外部服务并没有注册到注册中心中,需要通过直接地址调用,服务的直接地址可以通过@FeignClient注解的url参数指定:

@FeignClient(name = "app-server", url = "http://127.0.0.1:8101")
public interface AppServerFeign {

}

3.2 同一个服务的读写请求使用不同的配置

我们对同一个服务提供的不同接口的处理方式往往不同,比如只读接口或幂等性写接口调用失败时,我们往往会尝试重新请求,而对非幂等性接口调用则不进行重试,这时可以将该服务需要不同配置的接口分别放在不同的openfeign接口中,并通过contextId进行区分,如下所示(configuration参数将在下面介绍):


@FeignClient(name = "app-server", contextId = "app-server-read", configuration = AppServerFeignReadConfig.class)
public interface AppServerFeign {

}

@FeignClient(name = "app-server", contextId = "app-server-write", configuration = AppServerFeignWriteConfig.class)
public interface AppServerFeign {

}

3.3 解析404响应body

默认情况下,openfeign只有在请求响应码在[200, 300)范围内的响应body进行解析(实现该功能的类默认为ResponseEntityDecoder, 它会委托给SpringDecoder实现具体的解析功能),提取信息转换成接口定义的返回类对象,然后正常返回给调用者。如果请求的响应码为其他的值,则会调用ErrorDecoder进行处理,默认最后会抛出异常给调用者。如果希望响应码为404时依然使用ResponseEntityDecoder解析响应body,则可以设置dismiss404值为true

@FeignClient(name = "app-server", dismiss404 = true)
public interface AppServerFeign {

}

3.5 异常回调函数fallbackfallbackFactory

当openfeign接口调用抛出异常时,openfeign可以指定回调fallback方法,fallback方法中可以定义异常处理逻辑。

引入依赖:

    
        org.springframework.cloud
        spring-cloud-starter-circuitbreaker-resilience4j
    

添加配置,使能circuitbreaker

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true

编写fallback逻辑,fallback类实现了openfeign client接口:

@Component
public class AppServerFallback implements AppServerFeign {

    @Override
    public HelloResponse hello(String who, String type, HelloRequest request) {
        return new HelloResponse(who, type, "fallback");
    }

}

然后在@FeignClient直接中指定fallback实现类

@FeignClient(name = "app-server", fallback = AppServerFallback.class)
public interface AppServerFeign {

}

如果你想对不同的异常类型采取不同的善后处理逻辑,可以指定fallbackFactory,由fallbackFactory根据不同的异常生成不同的fallback类,对异常进行不同的处理。

@Component
static class AppServerFallbackFactory implements FallbackFactory {

    @Override
    public AppServerFallback create(Throwable cause) {
        // 在这里可以根据不同异常进行不同的处理,返回不同的异常处理实例
        return new AppServerFallback();
    }

}

@FeignClient(name = "app-server", fallbackFactory = AppServerFallbackFactory.class)
public interface AppServerFeign {

}

4 openfeign的configuration

3.2 节引入了@FeignClient注解的configuration配置,但是没有介绍它的功能和用法,这里对它进行详细的介绍。每一个openfeign client接口可以指定一个configuration类,这个配置类可以创建一些自定义的bean,用来替换掉默认的bean。且这些bean只对配置的openfeign client有效,不影响其他的openfeign client。因为每一个openfeign client有一个子容器(context),子容器中的bean只有当前openfeign client可见。

4.1 定义LoggerLevel

public class AppServerFeignConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }
}

@FeignClient(name = "app-server", configuration = AppServerFeignReadConfig.class)
public interface AppServerFeign {

}

注意,这个AppServerFeignConfig配置类不能添加@Component@Configuration等注解,从而让他添加到spring 父容器中,如果这样的话就是全局生效了,而不是只针对当前的openfeign client有效。

4.2 拦截器

用户可添加http拦截,查看或修改请求的host、header、请求参数、request body或响应body等信息。如下所示:

@Configuration
public class AppProperties {

    public static String appName;
    public static String token;

    @Value("${app.app-name}")
    public void setAppName(String appName) {
        DunetConfig.appName = appName;
    }

    @Value("${app.token}")
    public void setToken(String token) {
        DunetConfig.token = token;
    }

}

public class AppServerFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            String appName = AppProperties.appName;
            String token = AppProperties.token;
            requestTemplate.header("app-name", appName);
            long currentTime = System.currentTimeMillis();
            requestTemplate.header("time-stamp", Long.toString(currentTime));
            String authText = appName + currentTime + token;
            String auth = DigestUtils.md5DigestAsHex(authText.getBytes()).toLowerCase();
            requestTemplate.header("Authorization", auth);
            requestTemplate.header("Content-Type", "application/json");
            requestTemplate.header("Accept", "application/json");
        };
    }
}

4.3 重试

当http调用遇到临时性失败,如tcp连接或读写数据超时,或者服务有多个实例,为提高应用的可靠性,往往会对只读接口或幂等性写接口进行重试。openfeign也提供了重试机制,只要简单的配置即可。

4.3.1 tcp超时:ConnectionTimeout、ReadTimeout、WriteTimeout

tcp超时时,只需要配置重试策略即可

public class AppServerFeignConfig {

    @Bean
    public Retryer feignRetryer() {
        // 最大请求次数为3,重试次数为2,首次重试时间间隔100毫秒,重试时间间隔以1.5的指数递升,重试最大间隔时间为1s
        return new Retryer.Default(100, SECONDS.toMillis(1), 3);
    }

}

4.3.2 http异常响应码

如果http响应码不是[200, 300),而你又想对某些情况下进行重试时,可以自定义ErrorDecoder,在ErrorDecoder中根据条件抛出RetryableException,openfeign就会根据配置Retry策略进行重试了。下面的ErrorDecoder实现了对服务端错误进行重试的逻辑


public class AppServerFeignConfig {

    private final ErrorDecoder defaultErrorDecoder = new ErrorDecoder.Default();

    @Bean
    public Retryer feignRetryer() {
        // 最大请求次数为3,重试次数为2,首次重试时间间隔100毫秒,重试时间间隔以1.5的指数递升,重试最大间隔时间为1s
        return new Retryer.Default(100, SECONDS.toMillis(1), 3);
    }
    
    @Bean
    public ErrorDecoder feignError() {
        return (key, response) -> {
            if (response.status() >= 500) {
                FeignException exception = FeignException.errorStatus(key, response);
                return new RetryableException(
                        response.status(),
                        exception.getMessage(),
                        response.request().httpMethod(),
                        new Date(),
                        response.request());
            }

            // 其他异常交给Default去解码处理
            return defaultErrorDecoder.decode(key, response);
        };
    }
}

5 通过配置文件自定义openfeign的功能

上面通过@FeignClient注解的configuration类对openfeign的配置均能通过配置文件进行配置。除了上面介绍的部分配置,更全面的配置请参考FeignClientProperties以及FeignClientProperties.FeignClientConfiguration类。

配置文件配置请参考如下配置:

spring:
  cloud:
    openfeign:
      client:
        config:
          # app-server为feign client名字。如果值为default,表示默认配置或全局配置
          app-server:
            url: http://remote-service.com
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: full
            errorDecoder: com.example.SimpleErrorDecoder
            retryer: com.example.SimpleRetryer
            defaultQueryParameters:
                query: queryValue
            defaultRequestHeaders:
                header: headerValue
            requestInterceptors:
                - com.example.FooRequestInterceptor
                - com.example.BarRequestInterceptor
            responseInterceptor: com.example.BazResponseInterceptor
            dismiss404: false
            encoder: com.example.SimpleEncoder
            decoder: com.example.SimpleDecoder
            contract: com.example.SimpleContract
            capabilities:
                - com.example.FooCapability
                - com.example.BarCapability
            queryMapEncoder: com.example.SimpleQueryMapEncoder
            micrometer.enabled: false

6 指定底层http客户端组件

openfeign默认采用URLConnection作为http客户端,也可以配置使用比较常用的apache httpclientokhttp

6.1 使用apache httpclient

    
        io.github.openfeign
        feign-httpclient
    
spring:
  cloud:
    openfeign:
      httpclient:
        enabled: true
        max-connections: 200
        max-connections-per-route: 50
        time-to-live: 900
        time-to-live-unit: seconds

6.2 使用okhttp

    
        io.github.openfeign
        feign-okhttp
    
spring:
  cloud:
    openfeign:
      okhttp:
        enabled: true

7 参考文档

  • Spring Cloud OpenFeign
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论