声明式服务调用-Feign 基础篇

声明式服务调用-Feign 基础篇

文章目录

    • 声明式服务调用-Feign 基础篇
    • 前言
    • 项目环境
    • 1.什么是 Feign?
    • 2.原生 Feign API
    • 3.Spring Cloud OpenFeign
    • 4.Feign 整合 Hystrix
    • 5.Feign 的配置
      • 5.1 Feign 的配置-代码方式
      • 5.2 Feign 的配置-配置文件方式
    • 6.Feign 的使用技巧
      • 6.1 继承特性
      • 6.2 拦截器
      • 6.3 请求对象类型参数传递
      • 6.4 日志配置
      • 6.5 异常解码器
    • 7.参考

前言

本篇主要是介绍什么是 Feign, Feign 的基本使用方式以及一些使用技巧,让我们对 Feign 有一个全面的认识;后续的源码分析篇,我们再看看 Feign 是如何实现这些功能的,以及 Spring Cloud 如何集成 Feign。

项目环境

  • Java 8
  • Spring Cloud Greenwich.SR2
  • Spring Boot 2.1.6.RELEASE
  • 项目地址:https://github.com/huajiexiewenfeng/deep-in-spring-cloud-netflix

1.什么是 Feign?

Feign 是一个声明式的 REST 客户端,它的目的就是让 REST 调用更加简单。Feign 提供了 HTTP 请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。

而且 Feign 会完全代理 HTTP 请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Spring Cloud 对 Feign 进行了封装,使其支持 SpringMVC 标准注解和 HttpMessageConverters。Feign 可以与 Eureka 和 Ribbon 组合使用以支持负载均衡,与 Hystrix 组合使用,支持熔断回退。

如果你没有使用 Spring Cloud,那么可以直接用原生的 Feign 来调用 API,如果你使用了 Spring Cloud,可以直接用 Spring Cloud OpenFeign 来调用 API。

2.原生 Feign API

示例原地址(GitHub 官网):https://github.com/OpenFeign/feign

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
  String login;
  int contributions;
}

public static class Issue {
  String title;
  String body;
  List<String> assignees;
  int milestone;
  List<String> labels;
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");

    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

使用 GitHub 接口的方式来定义了两个方法,其中使用了两个注解

  • @RequestLine 可以类比为 @RequestMapping,用来定义请求的 URI;
  • @Param 可以类比为 @RquestParam,用来做参数的映射。

调用方式,通过 Feign 的 builder 模式构建了 GitHub 接口对象后,就可以直接通过 GiuHub 接口对象调用里面的 contributors 方法,然后可以得到返回结果,可以看到 Feign 调用还是很简便的,而且语义比较清晰,接口定义的方式也比较简洁。

3.Spring Cloud OpenFeign

Spring 团队在 Feign 的基础上进行了扩展,让我们的使用更加简单

  • 提供 Spring Web MVC 注解处理
  • 提供 Feign 自动装配

我们先来看一个简单的示例:

详细代码示例可以参考本项目 github 中的 feign-demo 模块

FeignClient API 层

@FeignClient("feign-user-service")
public interface UserServiceFeignApi {

    @GetMapping("/api/feign/getUser")
    User getUser(@RequestParam("id") Long id);

}

feign-client 服务调用者(消费者)

  • @Autowired 的方式依赖注入 UserServiceFeignApi 对象
    • 为什么接口可以注入?因为标注有 @FeignClient 注解的接口,框架会做处理,将接口替换代理对象,并注入到 IoC 容器中,这里通过依赖注入拿到的对象实际上是代理对象,后续的源码分析会讨论。
  • userServiceFeignApi 直接调用 getUser 方法(为了减少调用的层次,这里就不使用 Service 层了)
@RestController
public class UserController {

    @Autowired
    private UserServiceFeignApi userServiceFeignApi;

    @GetMapping("/feign/getUser")
    public User getUser(Long id) {
        User user = userServiceFeignApi.getUser(id);
        return user;
    }
}

启动类增加 @EnableFeignClientsbasePackages 看情况设置,保证可以扫描到标注有 @FeignClient 的相关接口

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages="com.csdn")
public class FeignClientBootstrap {
    public static void main(String[] args) {
        new SpringApplicationBuilder(FeignClientBootstrap.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

feign-user-service 服务生产者(提供者)

  • 实现 UserServiceFeignApi 接口即可
  • 如果不想实现接口,也可以写同样方法签名的方法,并且标注 @GetMapping("/feign/getUser") 同样的注解以及 URL(这种没有必要,也不方便)
@RestController
public class UserController implements UserServiceFeignApi {

    @Override
    public User getUser(Long id) {
        User user = new User();
        user.setId(id);
        user.setName("小仙");
        return user;
    }

}

分别启动 Eureka 注册中心、feign 客户端(feign-client)和生产者端(feign-user-service)

浏览器输入 http://127.0.0.1:8281/feign/getUser?id=2

结果如下:
声明式服务调用-Feign 基础篇_第1张图片
调用链路如下:

声明式服务调用-Feign 基础篇_第2张图片

4.Feign 整合 Hystrix

feign-client 应用引入 hystrix 依赖

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

application.yaml 加入下面的配置

feign:
  hystrix:
    enabled: true

这里有两种方式来设置 fallback 方法,选择其一即可。

  • 第一种:@FeignClient#fallback
@FeignClient(value = "feign-user-service",fallback = UserServiceFeignFallback.class)
public interface UserServiceFeignApi {

    @GetMapping("/api/feign/getUser")
    User getUser(@RequestParam("id") Long id);

}

UserServiceFeignFallback 代码如下:

@Component
public class UserServiceFeignFallback implements UserServiceFeignApi {
    @Override
    public User getUser(Long id) {
        return new User(id, "fallback");
    }
}
  • 第二种:@FeignClient#fallbackFactory
@FeignClient(value = "feign-user-service",fallbackFactory = UserServiceFeignFallbackFactory.class)
public interface UserServiceFeignApi {

    @GetMapping("/api/feign/getUser")
    User getUser(@RequestParam("id") Long id);

}

UserServiceFeignFallbackFactory 代码如下:

@Component
public class UserServiceFeignFallbackFactory implements FallbackFactory<UserServiceFeignApi> {

    @Override
    public UserServiceFeignApi create(Throwable cause) {
        return id -> new User(id, "fallback");
    }
}

关闭 feign-user-service 服务,浏览器输入 http://127.0.0.1:8281/feign/getUser?id=2

声明式服务调用-Feign 基础篇_第3张图片

可以看到返回了我们设置好的 fallbcak 信息。

5.Feign 的配置

5.1 Feign 的配置-代码方式

Feign 的配置在代码中可以指定,我们可以为每个 Feign Client 创建一个单独的配置类,在类中配置对应的信息,比如我们要改变日志的级别,可以配置一个 Logger.Level 的 Bean、访问对应的接口,可以在控制台看见详细的调用日志输出。前提是日志的级别必须为 Debug。还需要在 @FeignClient 注解中进行配置类的指定。

例如在 @FeignClient 注解中配置 configuration = CustomFeignConfig.class 指定我们的配置类。

@FeignClient(value = "feign-user-service",
        fallbackFactory = UserServiceFeignFallbackFactory.class,
        configuration = CustomFeignConfig.class)
public interface UserServiceFeignApi {

    @GetMapping("/api/feign/getUser")
    User getUser(@RequestParam("id") Long id);

}

CustomFeignConfig 自定义的配置类代码如下:

  • Logger 和 Contract 都是默认配置项,这里只是演示可以进行自定义
@Configuration
public class CustomFeignConfig {

    @Bean
    public Logger getLogger() {
        return new Slf4jLogger();
    }

    @Bean
    public Logger.Level getLoggerLevel() {
        return Logger.Level.FULL;
    }

    /**
     * 自定义 Contract
     * @return
     */
    @Bean
    public Contract feignContract() {
        // 使用 feign 默认注解
//        return new feign.Contract.Default();
        // 使用 springMvc 注解
        return new SpringMvcContract();
    }
    // feignDecoder,feignEncoder.....
}

5.2 Feign 的配置-配置文件方式

更灵活的配置方式应该是放入配置文件中,建议你在工作中将配置都放入配置文件中,那么 Feign 在配置文件中如何进行配置呢?

feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

当然也是有统一的格式的,而且格式也比较简单,前缀都是固定的 feign.client.config,后面接的是 Feign Client 的名称,也就是我们 @FeignClient 注解中的 value,以本文为例就是 feign-user-service。然后是对应的配置项,这样的配置就可以为不同的 Feign Client 配置不同的值。

6.Feign 的使用技巧

这里总结了下面 5 种使用技巧

  • 继承特性
  • 拦截器
  • 请求对象类型参数传递
  • 日志配置
  • 异常解码器

6.1 继承特性

第 3 小节示例的写法,将 UserServiceFeignApi 接口类作为一个单独的应用模块,消费者端和生产者端都使用同一个接口,生产者 implements 这个接口,而消费者端使用依赖注入的方式直接使用 UserServiceFeignApi 这个类,这样就可以保证接口方法的一致性。

6.2 拦截器

Feign 中提供了拦截器机制,我们可以添加自己的拦截器来实现某些场景下的需求。BasicAuth 在 Feign 中默认提供了拦截器,我们只需要配置一下就可以使用,

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }

如果我们需要自定义拦截器,可以参考 BasicAuth 的代码,只要实现 RequestInterceptor 接口,在 apply 方法中编写你自己的逻辑就可以了,通过 RequestTemplate 可以进行很多操作,比如添加指定的请求头信息,这个可以用在服务间传递某些信息的时候。

    @Bean
    public CustomRequestInterceptor customRequestInterceptor() {
        return new CustomRequestInterceptor();
    }

CustomRequestInterceptor 自定义拦截器

public class CustomRequestInterceptor implements RequestInterceptor {

	@Override
	public void apply(RequestTemplate template) {
		System.err.println(template.url());
		template.header("myHeader", "blog.csdn.net");
	}

}

6.3 请求对象类型参数传递

注意版本 spring-cloud-starter-openfeign 2.1.0 +,使用 @SpringQueryMap 注解

@FeignClient(value = "feign-user-service")
public interface UserServiceFeignApi {

    @GetMapping("/api/feign/addUser")
    String addUser(@SpringQueryMap User user);
}

6.4 日志配置

当 API 调用失败后,需要有详细的请求信息来分析失败原因,我们可以设置 Feign 的日志级别来输出详细的请求信息,Feign 的日志级别有四种:

  • NONE 表示不输出日志

  • BASIC 表示只输出请求方法的 URL 和响应的状态码以及执行的时间

  • HEADERS 将 BASIC 信息和请求头信息输出

  • FULL 会输出全部完整的请求信息

了解了日志级别后,我们就可以为 Feign Client 设置不同的级别了,级别不同输出的请求信息的详细程度也不一样,后面的课时我会介绍动态的去调整日志级别,这样在平时是不输出日志的,一旦需要排查问题的时候就可以动态的将日志打开,非常方便。

6.5 异常解码器

Feign 中提供了异常的解码器,但我们也可以自定义异常解码器,自定义异常解码器可以用于内部服务之间调用的异常传递。比如说 A 服务调用 B 服务,B 服务中出现异常后,会由 B 服务中的全局异常处理器进行处理,然后返回给 A 服务的数据格式是固定的 code 是多少,message 是什么。

在 A 服务中,我们就可以通过 B 服务返回的 code 码来做对应的判断,调用是成功了,还是失败了。一般内部服务之间为了简便性,希望跟调用本地方法一样,当被调用方抛出异常后,调用方也能感知到对应的异常,这个时候可以在异常解码器中获取异常信息,然后转换成对应的异常对象返回。

@Component
public class FeignClientErrorDecoder implements ErrorDecoder {

	@Override
	public Exception decode(String methodKey, Response response) {
		System.out.println(methodKey + "\t" + response.status());
		try {
			String errorContent = Util.toString(response.body().asReader());
			return new Exception(errorContent);
		} catch (IOException e) {
			return new Exception(e.getMessage());
		}
	}

}

通过 @Component 注入到 Spring IoC 容器中,当然也可以通过配置 Bean 的方式。

    @Bean
    public ErrorDecoder errorDecoder(){
        return new FeignClientErrorDecoder();
    }

测试

feign-user-service 服务增加一个方法,详细代码可以参考 github 中的示例

    @Override
    public User getException(Long id) throws Exception {
        throw new Exception("故意抛出异常");
    }

浏览器输入 http://127.0.0.1:8281/feign/getException?id=1,控制台异常打印如下:

声明式服务调用-Feign 基础篇_第4张图片

7.参考

  • 《深入理解 Spring Cloud 与微服务架构》 方志朋

  • 《300分钟搞懂 Spring Cloud》尹吉欢

你可能感兴趣的:(Spring,Cloud,系列,Feign)