feign

使用Eureka作为服务注册中心,在服务启动后,各个微服务会将自己注册到Eureka server。那么服务之间是如何调用?又是如何进行负载均衡的呢?
目前,在Spring cloud 中服务之间通过restful方式调用有两种方式

  • restTemplate+Ribbon
  • feign

从实践上看,采用feign的方式更优雅(feign内部也使用了ribbon做负载均衡)。

一、什么是 feign?

Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。

  • Feign 支持Ribbon的负载均衡
  • Feign 集成了Hystrix(服务熔断)
  • Feign 采用的是基于接口配置
  • 支持可插拔的HTTP编码器和解码器
  • 支持HTTP请求和响应的压缩

二、Feign解决了什么问题?

封装了Http调用流程,更适合面向接口化的变成习惯
在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnectionApache HttpComponnetsOkHttp3Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:

image.png

三、Feign是如何设计的?

image.png

四、RestTemplate + Ribbon与Feign(自带Ribbon)的比较

角度 RestTemplate + Ribbon Feign(自带Ribbon)
可读性、可维护性 欠佳(无法从URL直观了解这个远程调用是干什么的) 极佳(能在接口上写注释,方法名称也是可读的,能一眼看出这个远程调用是干什么的)
开发体验 欠佳(拼凑URL不幸福) 极佳(漂亮的代码)
风格一致性 欠佳(本地API调用和RestTemplate调用的代码风格截然不同) 极佳(完全一致,不点开Feign的接口,根本不会察觉这是一个远程调用而非本地API调用)
性能 较好 中等(性能是RestTemplate的50%左右;如果为Feign配置连接池,性能可提升15%左右)
灵活性 极佳 中等(内置功能能满足大多数项目的需求)

Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。由于默认情况下,Feign采用的是JDK的HttpURLConnection,所以整体性能并不高。

几种Http框架的抽象层次

五、使用Feign

1. 导入依赖

    <dependencies>
        <!--openfein的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
    </dependencies>

2. 启用Feign

启用类上添加注解@EnableFeignClients客户端允许开启使用Feign调用,扫描@FeignClient标注的FeignClient接口

@SpringBootApplication
@EnableFeignClients(basePackages = { "com.xxx.xxx.xxx" })//开启feign,封装http的rest请求。可以用basePackages = { "com.xxx.xxx.xxx" }指定包扫描@FeignClient标注的FeignClient接口
//@EnableDiscoveryClient和@EnableEurekaClient共同点就是:都是能够让注册中心能够发现,扫描到改服务。
//不同点:@EnableEurekaClient只适用于Eureka作为注册中心,@EnableDiscoveryClient 可以是其他注册中心。
@EnableEurekaClient
//@EnableDiscoveryClient
public class FeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class,args);
    }
}

3. 编写FeignClient接口

@FeignClient( value = "hello-service-provider" ) 声明的方式指向了 服务提供者,而接口方法则实现了对服务提供者接口的实际调用

@FeignClient(value = "hello-service-provider")
public interface SchedualServiceHi {

    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam("name") String name);
}

注意:这里服务名不区分大小写,所以使用hello-service-provider和HELLO-SERVICE-PROVIDER都是可以的。另外,在Brixton.SR5版本中,原有的serviceId属性已经被废弃,若要写属性名,可以使用name或value。

FeignClient注解的一些属性

属性名 默认值 作用 备注
value 空字符串 调用服务名称,和name属性相同
serviceId 空字符串 服务id,作用和name属性相同 已过期
name 空字符串 调用服务名称,和value属性相同
url 空字符串 全路径地址或hostname,http或https可选
decode404 false 配置响应状态码为404时是否应该抛出FeignExceptions
configuration {} 自定义当前feign client的一些配置 参考FeignClientsConfiguration
fallback void.class 熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。 底层依赖hystrix,启动类要加上@EnableHystrix
path 空字符串 自动给所有方法的requestMapping前加上前缀,类似与controller类上的requestMapping

4. 实现对Feign客户端的调用

接着,创建一个RestClientController来实现对Feign客户端的调用。使用@Autowired直接注入上面定义的HelloServiceFeign实例,并在postPerson函数中调用这个绑定了hello-service-provider服务接口的客户端来向该服务发起/demo/getHost和/demo/postPerson接口的调用。

@RestController
public class HiController {

    @Autowired
    SchedualServiceHi schedualServiceHi;

    @GetMapping(value = "/test/hi")
    public String sayHi(@RequestParam String name){
        return schedualServiceHi.sayHiFromClientOne(name);

    }
}

5. 需要在application.yml中指定服务注册中心,并定义自身的服务名为service-feign,端口使用8765。

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8765
spring:
  application:
    name: service-feign

六、其它

1. Feign自定义处理返回的异常

实现Feign的 ErrorDecoder 的接口就可以实现 http请求层面的错误处理。
StashErrorDecoder类实现了ErrorDecoder接口。在Feign客户端发生http请求层面的错误时会调用decode方法。在decode方法中实现自定义的错误处理。

public class StashErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() >= 400 && response.status() <= 499) {
            //这里是给出的自定义异常
            return new StashClientException(
                    response.status(),
                    response.reason()
            );
        }
        if (response.status() >= 500 && response.status() <= 599) {
            //这里是给出的自定义异常
            return new StashServerException(
                    response.status(),
                    response.reason()
            );
        }
        //这里是其他状态码处理方法
        return errorStatus(methodKey, response);
    }
}
  • 接下来就需要注册这个错误拦截器了,如果是直接手动构建FeignClient的使用方法。那需要在构建客户端时指定errorDecoder
return Feign.builder()
                .errorDecoder(new StashErrorDecoder())
                .target(StashApi.class, url);
  • 如果是使用了spring-cloud-open-feign的使用方式,包含了启动的自动配置,所以只需要将错误处理类注册为配置类,即添加@Configuration注解即可。
@Configuration
public class StashErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        // 实现代码
    }
}

2. Feign使用OKhttp发送request

Feign底层默认是使用jdk中的HttpURLConnection发送HTTP请求,feign也提供了OKhttp来发送请求,具体配置如下:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
  okhttp:
    enabled: true
  hystrix:
    enabled: true

3. Feign开启GZIP压缩

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率。

feign:
  compression:
    request: #请求
      enabled: true #开启
      mime-types: text/xml,application/xml,application/json #开启支持压缩的MIME TYPE
      min-request-size: 2048 #配置压缩数据大小的下限
    response: #响应
      enabled: true #开启响应GZIP压缩

注意:
由于开启GZIP压缩之后,Feign之间的调用数据通过二进制协议进行传输,返回值需要修改为ResponseEntity<byte[]>才可以正常显示,否则会导致服务之间的调用乱码。

示例如下:

@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);

4. 作用在所有Feign Client上的配置方式

方式一:通过java bean 的方式指定。
@EnableFeignClients注解上有个defaultConfiguration属性,可以指定默认Feign Client的一些配置。

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class ProductApplication {

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

DefaultFeignConfiguration内容:

@Configuration
public class DefaultFeignConfiguration {

    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(1000,3000,3);
    }
}

方式二:通过配置文件方式指定。

feign:
  client:
    config:
      default:
        connectTimeout: 5000 #连接超时
        readTimeout: 5000 #读取超时
        loggerLevel: basic #日志等级

5. Feign Client开启日志

日志配置和上述配置相同,也有两种方式。
方式一:通过java bean的方式指定

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

方式二:通过配置文件指定

logging:
  level:
    com.xt.open.jmall.product.remote.feignclients.CartFeignClient: debug

6. Feign 的GET的多参数传递

目前,feign不支持GET请求直接传递POJO对象的,目前解决方法如下:

  • 把POJO拆散城一个一个单独的属性放在方法参数中
  • 把方法参数编程Map传递
  • 使用GET传递@RequestBody,但此方式违反restful风格

介绍一个最佳实践,通过feign的拦截器来实现。

@Component
@Slf4j
public class FeignCustomRequestInteceptor implements RequestInterceptor {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        if (HttpMethod.GET.toString() == template.method() && template.body() != null) {
            //feign 不支持GET方法传输POJO 转换成json,再换成query
            try {
                Map<String, Collection<String>> map = objectMapper.readValue(template.bodyTemplate(), new TypeReference<Map<String, Collection<String>>>() {

                });
                template.body(null);
                template.queries(map);
            } catch (IOException e) {
                log.error("cause exception", e);
            }
        }
    }

七、总结

总到来说,Feign的源码实现的过程如下:

  • @EnableFeignCleints注解开启FeignCleint
  • 启动时,程序会进行包扫描,扫描所有包下所有@FeignClient注解的类,并将这些类注入到spring的IOC容器中。当定义的Feign中的接口被调用时,通过JDK的动态代理来生成RequestTemplate。
  • RequestTemplate中包含请求的所有信息,如请求参数,请求URL等。
  • RequestTemplate声明Request,然后将Request交给client处理,这个client默认是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
  • 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。