本篇主要是介绍什么是 Feign, Feign 的基本使用方式以及一些使用技巧,让我们对 Feign 有一个全面的认识;后续的源码分析篇,我们再看看 Feign 是如何实现这些功能的,以及 Spring Cloud 如何集成 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。
示例原地址(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 接口的方式来定义了两个方法,其中使用了两个注解
调用方式,通过 Feign 的 builder 模式构建了 GitHub 接口对象后,就可以直接通过 GiuHub 接口对象调用里面的 contributors 方法,然后可以得到返回结果,可以看到 Feign 调用还是很简便的,而且语义比较清晰,接口定义的方式也比较简洁。
Spring 团队在 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 服务调用者(消费者)
@RestController
public class UserController {
@Autowired
private UserServiceFeignApi userServiceFeignApi;
@GetMapping("/feign/getUser")
public User getUser(Long id) {
User user = userServiceFeignApi.getUser(id);
return user;
}
}
启动类增加 @EnableFeignClients
, basePackages
看情况设置,保证可以扫描到标注有 @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 服务生产者(提供者)
@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-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
可以看到返回了我们设置好的 fallbcak 信息。
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 自定义的配置类代码如下:
@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.....
}
更灵活的配置方式应该是放入配置文件中,建议你在工作中将配置都放入配置文件中,那么 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 配置不同的值。
这里总结了下面 5 种使用技巧
第 3 小节示例的写法,将 UserServiceFeignApi 接口类作为一个单独的应用模块,消费者端和生产者端都使用同一个接口,生产者 implements 这个接口,而消费者端使用依赖注入的方式直接使用 UserServiceFeignApi 这个类,这样就可以保证接口方法的一致性。
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");
}
}
注意版本 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);
}
当 API 调用失败后,需要有详细的请求信息来分析失败原因,我们可以设置 Feign 的日志级别来输出详细的请求信息,Feign 的日志级别有四种:
NONE 表示不输出日志
BASIC 表示只输出请求方法的 URL 和响应的状态码以及执行的时间
HEADERS 将 BASIC 信息和请求头信息输出
FULL 会输出全部完整的请求信息
了解了日志级别后,我们就可以为 Feign Client 设置不同的级别了,级别不同输出的请求信息的详细程度也不一样,后面的课时我会介绍动态的去调整日志级别,这样在平时是不输出日志的,一旦需要排查问题的时候就可以动态的将日志打开,非常方便。
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
,控制台异常打印如下:
《深入理解 Spring Cloud 与微服务架构》 方志朋
《300分钟搞懂 Spring Cloud》尹吉欢