前言

Feign、OpenFeign及SpringCloud Feign的区别

Feign是Spring Cloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用接口,就可以调用服务注册中心的服务。

由于 Netflix 公司不再维护feign,feign由社区维护,feign更名为 openfeign,并且项目迁移到新的仓库。后续版本仅使用“io.github.openfeign”,推荐使用该依赖。

spring-cloud-openfeign 是基于 openfeign 进行包装,集成了 SpringMVC 的注解等方便SpringBoot项目开发的一个组件。



Spring Cloud OpenFeign简介

Spring Cloud OpenFeign是一个声明式的 HTTP客户端,它简化了HTTP客户端的开发,使编写Web服务的客户端变得更容易。使用Spring Cloud OpenFeign,只需要创建一个接口并注解,就能很容易地调用各服务提供的HTTP接口。Spring Cloud OpenFeign基于OpenFeign实现,它除了提供声明式的 HTTP客户端外,还整合了Spring Cloud Hystrix,能够轻松实现熔断器模型。

Spring Cloud对OpenFeign进行了增强,使得Spring Cloud OpenFeign支持Spring MVC注解。同时,Spring Cloud整合了Ribbon和 Eureka,这让 Spring Cloud OpenFeign的使用更加方便。

Spring Cloud OpenFeign能够帮助我们定义和实现依赖服务接口。在Spring Cloud OpenFeign的帮助下,只需要创建一个接口并用注解方式配置它,就可以完成服务提供方的接口绑定,减少在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。



Spring Cloud OpenFeign快速入门

我们创建一个父子项目,项目结构如下:

Spring Cloud OpenFeign介绍与使用_eureka

模块说明:

  • eureka-server是注册中心,使用的是Eureka,如何搭建,可以参考:注册中心Eureka的介绍和使用
  • openfeign-provider是服务提供者,注册到eureka注册中心。
  • openfeign-consumer是服务消费者,我们使用openfeign功能都在这个模块,也是注册到eureka注册中心。

父pom.xml:

<!--依赖管理必须加-->
<dependencyManagement>
	<dependencies>
		<!--SpringBoot 依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-dependencies</artifactId>
			<version>2.2.5.RELEASE</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
		<!--Spring Cloud 依赖-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>Hoxton.SR3</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

下面就贴出各个模块的主要代码。



eureka-server

注册中心。

pom.xml:

<!--这个依赖可以不加,eureka有的-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--Spring Cloud 的 eureka-server 依赖 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

application.yml:

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com
    #eureka的client注册到server时默认是使用hostname而不是ip
    #优先ip-address注册
    prefer-ip-address: true
  client:
    register-with-eureka: false     #false表示不向注册中心注册自己。
    fetch-registry: false     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      #集群指向其它eureka
      #defaultZone: http://eureka7002.com:7002/eureka/
      #单机就是7001自己
      #defaultZone: http://eureka7001.com:7001/eureka/
      defaultZone: http://eureka7001.com:${server.port}/eureka/
    #server:
    #关闭自我保护机制,保证不可用服务被及时踢除
    #enable-self-preservation: false
    #eviction-interval-timer-in-ms: 2000

启动类:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApp {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApp.class, args);
    }
}



openfeign-provider

服务提供者。

pom.xml:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml:

server:
  port: 8001

spring:
  application:
    name: feign-provider

eureka:
  instance:
    #eureka的client注册到server时默认是使用hostname而不是ip
    #优先ip-address注册
    prefer-ip-address: true
    # SpringCloud 2.0 已经改成 ${spring.cloud.client.ip-address}了
    # 不能使用${spring.cloud.client.ipAddress}
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #单机版, 注意:使用域名注册, 必须配置注册中心的ip与域名的映射关系,windows在hosts配置
      defaultZone: http://eureka7001.com:7001/eureka

启动类:

@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApp {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApp.class, args);
    }
}

实体UserInfo:

public class UserInfo implements Serializable {
    private Long id;
    private Long userId;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "id=" + id +
                ", userId=" + userId +
                ", name='" + name + '\'' +
                '}';
    }
}

controller(被调用方):

@RestController
@RequestMapping("/provider")
public class ProviderController {

    //无参
    //GET http://localhost:8001/provider/helloNoParam
    //POST http://localhost:8001/provider/helloNoParam
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloNoParam")
    public String helloNoParam() {
        return "helloNoParam openfeign";
    }

    //单个参数
    //GET http://localhost:8001/provider/helloPathVar/harvey
    //POST http://localhost:8001/provider/helloPathVar/harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloPathVar/{name}")
    public String helloPathVar(@PathVariable("name") String name) {
        return "helloPathVar " + name;
    }

    //单个参数
    //GET http://localhost:8001/provider/helloParam?name=harvey
    //POST http://localhost:8001/provider/helloParam?name=harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloParam")
    public String helloParam(@RequestParam("name") String name) {
        return "helloParam " + name;
    }

    //多个参数
    //GET http://localhost:8001/provider/helloMultiParam?id=1&name=harvey
    //POST http://localhost:8001/provider/helloMultiParam?id=1&name=harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloMultiParam")
    public String helloMultiParam(@RequestParam("id") Long id, @RequestParam("name") String name) {
        return "hellMultiParam id=" + id + "and name=" + name;
    }

    //多个参数
    // Content-Type application/x-www-form-urlencoded,form表单提交数据默认就是这种
    // POST http://localhost:8001/provider/helloMultiParamForObject?id=1&userId=10001&name=harvey 正常
    // GET http://localhost:8001/provider/helloMultiParamForObject?id=1&userId-10001&name=harvey 正常
    // 存在问题:这种方式只能保证前端传的参数与后端参数完全一致,即如果前端传下划线,后端字段使用驼峰接收,则无法接收
    // 入参必须使用实体类明确指定字段,如UserInfo,不能使用Map这种类型,否则会接收不到数据
    @RequestMapping(value = "/helloMultiParamForObject")
    public String helloMultiParamForObject(UserInfo userInfo) {
        return "helloMultiParamForObject " + userInfo;
    }

    //多个参数
    //Content-Type application/json
    //POST http://localhost:8001/provider/helloMultiParamForBody
    @RequestMapping(value = "/helloMultiParamForBody", method = RequestMethod.POST)
    public String helloMultiParamForBody(@RequestBody UserInfo userInfo) {
        return "helloMultiParamForBody " + userInfo;
    }
}



openfeign-consumer

服务消费者。

pom.xml:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!--spring openfeign-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

application.yml:

server:
  port: 9901

spring:
  application:
    name: feign-consumer

eureka:
  instance:
    #eureka的client注册到server时默认是使用hostname而不是ip
    #优先ip-address注册
    prefer-ip-address: true
    # SpringCloud 2.0 已经改成 ${spring.cloud.client.ip-address}了
    # 不能使用${spring.cloud.client.ipAddress}
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #单机版, 注意:使用域名注册, 必须配置注册中心的ip与域名的映射关系,windows在hosts配置
      defaultZone: http://eureka7001.com:7001/eureka

启动类:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApp {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApp.class, args);
    }
}

记得添加@EnableFeignClients注解,扫描那些包含@FeignClient注解的接口。

实体类UserInfo:

public class UserInfo implements Serializable {
    private Long id;
    private Long userId;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "id=" + id +
                ", userId=" + userId +
                ", name='" + name + '\'' +
                '}';
    }
}

主要是字段要与服务提供方的字段保持一致。

feign接口:

/**
 * feign推荐不要使用@GetMapping或@PostMapping,直接用@RequestMapping,可以避免一些莫名其妙的坑,同时建议明确请求的方式,当然我这里没有~~~
 */
@FeignClient(name = "feign-provider")
public interface ProviderClient {

    //无参
    @RequestMapping("/provider/helloNoParam")
    String helloNoParam();

    //单个参数
    @RequestMapping("/provider/helloPathVar/{name}")
    String helloPathVar(@PathVariable("name") String name);

    //单个参数
    @RequestMapping("/provider/helloParam")
    String helloParam(@RequestParam("name") String name);

    //多个参数
    @RequestMapping("/provider/helloMultiParam")
    String helloMultiParam(@RequestParam("id") Long id, @RequestParam("name") String name);

    //多个参数
    //这里传的是一个对象,一种是实体类,一个是Map
    //如果是实体类,服务提供方的controller可以使用相同的实体类作为入参,这样可以接收到参数
    //如果是Map,服务提供方的controller必须使用@RequestParam平铺列出每个参数, 否则因Controller的原因会导致接收不到参数
    @RequestMapping(value = "/provider/helloMultiParamForObject", method = RequestMethod.GET)
    String helloMultiParamForObject(@SpringQueryMap UserInfo userInfo);

    //多个参数
    @RequestMapping(value = "/provider/helloMultiParamForBody", method = RequestMethod.POST)
    String helloMultiParamForBody(@RequestBody Map<String, Object> userInfo);
}

注:我们这里都是使用@RequestMapping注解,不可以使用@RequestLine和@Headers组合替换,这种是Feign自己的契约,不是SpringMVC的。

controller(调用方):

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    private ProviderClient providerClient;

    //无参
    //GET http://localhost:9901/consumer/helloNoParam
    //POST http://localhost:9901/consumer/helloNoParam
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloNoParam")
    public String helloNoParam() {
        return providerClient.helloNoParam();
    }

    //单个参数
    //GET http://localhost:9901/consumer/helloPathVar/harvey
    //POST http://localhost:9901/consumer/helloPathVar/harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloPathVar/{name}")
    public String helloPathVar(@PathVariable("name") String name) {
        return providerClient.helloPathVar(name);
    }

    //单个参数
    //GET http://localhost:9901/consumer/helloParam?name=harvey
    //POST http://localhost:9901/consumer/helloParam?name=harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloParam")
    public String helloParam(@RequestParam("name") String name) {
        return providerClient.helloParam(name);
    }

    //多个参数
    //GET http://localhost:9901/consumer/helloMultiParam?id=1&name=harvey
    //POST http://localhost:9901/consumer/helloMultiParam?id=1&name=harvey
    //Content-Type application/x-www-form-urlencoded
    @RequestMapping("/helloMultiParam")
    public String helloMultiParam(@RequestParam("id") Long id, @RequestParam("name") String name) {
        return providerClient.helloMultiParam(id, name);
    }

    //多个参数
    // Content-Type application/x-www-form-urlencoded,form表单提交数据默认就是这种
    // POST http://localhost:9901/consumer/helloMultiParamForObject?id=1&userId=10001&name=harvey 正常
    // GET http://localhost:9901/consumer/helloMultiParamForObject?id=1&userId-10001&name=harvey 正常
    // 存在问题:这种方式只能保证前端传的参数与后端参数完全一致,即如果前端传下划线,后端字段使用驼峰接收,则无法接收
    // 入参必须使用实体类明确指定字段,如UserInfo,不能使用Map这种类型,否则会接收不到数据
    @RequestMapping(value = "/helloMultiParamForObject")
    public String helloMultiParamForObject(UserInfo userInfo) {
        return providerClient.helloMultiParamForObject(userInfo);
    }

    //多个参数
    //Content-Type application/json
    //POST http://localhost:9901/consumer/helloMultiParamForBody
    @RequestMapping(value = "/helloMultiParamForBody", method = RequestMethod.POST)
    public String helloMultiParamForBody(@RequestBody Map<String, Object> userInfo) {
        return providerClient.helloMultiParamForBody(userInfo);
    }
}

 



OpenFeign工作原理

  • 添加@EnableFeignClients注解开启对@FeignClient注解的扫描加载处理。根据Feign Client的开发规范,定义接口并添加@FeiginClient注解。
  • 当程序启动之后,会进行包扫描,扫描所有@FeignClient注解的接口,并将这些信息注入到IOC容器中。当定义的Feign接口被调用时,通过JDK的代理的方式生成具体的RequestTemplate。Feign会为每个接口方法创建一个RequestTemplate对象。该对象封装了HTTP请求需要的所有信息,例如请求参数名、请求方法等信息。
  • 然后由RequestTemplate生成Request,把Request交给Client去处理,这里的Client可以是JDK原生的URLConnection、HttpClient或Okhttp。最后Client被封装到LoadBalanceClient类,看这个类的名字既可以知道是结合Ribbon负载均衡发起服务之间的调用,因为在OpenFeign中默认是已经整合了Ribbon了。


OpenFiegn的基础功能



剖析@FeignClient注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    String contextId() default "";

    @AliasFor("value")
    String name() default "";

    String qualifier() default "";

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {};

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";

    boolean primary() default true;
}

从FeignClient的注解可以看得出,ElementType.TYPE说明FeignClient的作用目标是接口。其常用的属性如下:

  • name:执行FeignClient的名称,如果项目中使用Ribbon,name属性会作为微服务的名称,用作服务发现。
  • url:url一般用于调试,可以手动指定@FeignClient调用的地址。
  • decode404:当发生404错误时,如果该字段为true,会调用decoder进行解码,否则抛出FeignException。
  • configuration:Feigin配置类,可自定义Feign的Encode,Decode,LogLevel,Contract,默认的配合类是FeignClientsConfiguration。
  • fallback:定义容错的类,当远程调用的接口失败或者超时的时候,会调用对应接口的容错罗杰,fallback执行的类必须实现@FeignClient标记的接口。在OpenFeign的依赖中可以发现,集成Hystrix。
  • fallbackFactory:工厂类,用于生成fallback类实例,通过此属性可以实现每个接口通用的容错逻辑,以达到减少重复的代码。
  • path:定义当前FeignClient的统一前缀。


自定义 Feign 配置类

在 Spring Cloud 中,OpenFeign默认的配置类为 FeignClientsConfiguration。

默认注入了很多 Feign 相关的配置 Bean,包括 FeignRetryer、 FeignLoggerFactory 和 FormattingConversionService 等。另外,Decoder、Encoder 和 Contract 这 3 个类在没有 Bean 被注入的情况下,会自动注入默认配置的 Bean,即 ResponseEntity Decoder、SpringEncoder 和 SpringMvcContract。

如果你不知道如何自己定义配置时,你可以点进去看看人家默认配置是如何实现的。

在 Spring Cloud 中,你可以通过 @FeignClient 注解声明额外的配置(比 FeignClientsConfiguration 级别高)去控制 feign 客户端。

如果需要自定义单个Feign配置,不需要使用@Configuration 注解,如果加上了,它将全局生效。

/**
 * feign 客户端的自定义配置
 */
public class MyConfiguration {
    
}

@FeignClient接口添加上配置类

@FeignClient(name = "feign-provider", configuration = MyConfiguration.class)
public interface ProviderClient {
    
}



FeignClient开启日志

Feign为每一个Feign都提供了一个fegin.Logger实例。可以在配置中开启日志输出,开启的步骤也很简单。

第一步:在配置文件中配置日志输出

logging:
  level:
    # 指定那个FeignClient接口的请求需要输出日志,以及日志级别
    com.harvey.consumer.feign.ProviderClient: debug

第二步:通过Java代码的方式在主程序入口配置日志Bean

/**
 * feign 客户端的自定义配置
 */
public class MyConfiguration {

    /**
     * Logger.Level 的具体级别如下:
     * NONE:不记录任何信息
     * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
     * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
     * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

 

记得要在@FeignClient注解的configuration属性上指向该配置类。如果添加了@Configuration注解就可以不用指明了。



OpenFeign开始GZIP压缩

OpenFeign支持对请求和响应进行GZIP压缩,以此来提供通信效率。只需在配置文件中配置即可,比较简单。

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



重试机制的自定义

public class MyConfiguration {

    /**
     * 自定义重试机制
     * @return
     */
    @Bean
    public Retryer feignRetryer() {
        //feign提供的默认实现,最大请求次数为5,初始间隔时间为100ms,下次间隔时间1.5倍递增,重试间最大间隔时间为1s,
        return new Retryer.Default();
    }

}



ErrorDecoder-错误解码器的自定义

当 feign 调用返回 HTTP 报文时,会触发这个方法,方法内可以获得 HTTP 状态码,可以用来定制一些处理逻辑等等。

/**
 * feign 客户端的自定义配置
 */
@Slf4j
public class MyConfiguration {

    @Bean
    public ErrorDecoder feignError() {
        return (key, response) -> {
            if (response.status() == 400) {
                log.error("请求xxx服务400参数错误,返回:{}", response.body());
            }

            if (response.status() == 409) {
                log.error("请求xxx服务409异常,返回:{}", response.body());
            }

            if (response.status() == 404) {
                log.error("请求xxx服务404异常,返回:{}", response.body());
            }

            // 其他异常交给Default去解码处理
            // 这里使用单例即可,Default不用每次都去new
            return new ErrorDecoder.Default().decode(key, response);
        };
    }
}

采用了 lambda 的写法,response 变量是 Response 类型,通过 status()方法可以拿到返回的 HTTP 状态码,body()可以获得到响应报文。

这里用到了lombok,需要添加依赖:

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.2</version>
	<scope>provided</scope>
</dependency>



Feign 拦截器实践

拦截器在请求发出之前执行,在拦截器代码里可以修改请求参数,header 等等,如果你有签名生成的需求,可以放在拦截器中来实现。

/**
 * feign 客户端的自定义配置
 */
@Slf4j
public class MyConfiguration {


    @Bean
    public RequestInterceptor interceptor(){
       return (template) -> {
           ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

           if (attributes != null) {
               HttpServletRequest request = attributes.getRequest();
               template.header("sessionId", request.getHeader("sessionId"));
           }
           switch (template.method()) {
               case "GET":
                   log.info("{}OpenFeign GET请求,请求路径:【{}", System.lineSeparator(), template.url()+ "】");
                   break;
               case "POST":
                   log.info("{}OpenFeign POST请求,请求路径:【{}】,请求参数:【{}】",
                           System.lineSeparator(),
                           template.url(),
                           new String(template.body(), template.requestCharset()));
                   break;
               default:
           }
       };
    }
}



编解码器Encoders & Decoders

feign支持使用Gson和Jackson作为编解码工具,在创建api接口时通过encoder()和decoder()方法分别指定。在使用不同的类型时,需要引入不同的依赖 feign-jackson 或 feign-gson 。

feign中,默认的解码器只能解码类型为Response, String, byte[], void的返回值,若返回值包含其他类型,就必须指定解码器Decoder。并且希望在解码前对返回值进行前置处理,则需要使用mapAndDecode方法,设置处理逻辑。

同时,在发送请求时使用的post方法,方法参数为String或者byte[],并且参数没有使用注解,这时就需要添加Content-Type请求头并通过encoder()方法设置编码器,来发送类型安全的请求体。

使用feign-jackson

1、添加依赖

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-jackson</artifactId>
	<version>10.7.4</version>
</dependency>

2、配置编解码器

@Slf4j
public class MyConfiguration {

    @Bean
    public JacksonDecoder feignDecoder() {
        return new JacksonDecoder();
    }

    @Bean
    public JacksonEncoder feignEncoder() {
        return new JacksonEncoder();
    }
}

使用fastjson

1、添加依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.79</version>
</dependency>

2、FastJSONConfig完整配置

public class FastJSONConfig {

    public static StringHttpMessageConverter getStringHttpMessageConverter() {
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        // 解决Controller的返回值为String时,中文乱码问题
        stringConverter.setDefaultCharset(Charset.forName("UTF-8"));
        return stringConverter;
    }

    public static FastJsonHttpMessageConverter getFastJsonHttpMessageConverter() {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(
                SerializerFeature.DisableCircularReferenceDetect, // 防止循环引用
                SerializerFeature.WriteNullListAsEmpty, // 空集合返回[],不返回null
                // 空字符串返回"",不返回null
                // SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteMapNullValue);
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");// 时间格式
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        // 支持的类型。 虽然默认 已经指出全部类型了,但是还是指定fastjson 只用于json格式的数据,避免其他格式数据导致的异常
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON);
        fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
        fastJsonHttpMessageConverter.setDefaultCharset(Charset.forName("UTF-8"));
        return fastJsonHttpMessageConverter;
    }
}

3、配置编解码器

/**
 * feign 客户端的自定义配置
 */
@Slf4j
public class MyConfiguration {
    
    // 其实 这个解析不用配置也是可以的。因为只是解析 而已,用 jackJSON 一般没问题的
    @Bean
    public ResponseEntityDecoder feignDecoder() {
        HttpMessageConverter fastJsonConverter = FastJSONConfig.getFastJsonHttpMessageConverter();
        StringHttpMessageConverter stringConverter = FastJSONConfig.getStringHttpMessageConverter();
        ObjectFactory<HttpMessageConverters> objectFactory =
                () -> new HttpMessageConverters(stringConverter, fastJsonConverter);
        return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
    }

    @Bean
    public SpringEncoder feignEncoder(List<HttpMessageConverter<?>> converters) {
        // 注意 fastJsonConverter 必须指定 MediaType 否则会报错
        HttpMessageConverter fastJsonConverter = FastJSONConfig.getFastJsonHttpMessageConverter();
        StringHttpMessageConverter stringConverter = FastJSONConfig.getStringHttpMessageConverter();
        ObjectFactory<HttpMessageConverters> objectFactory =
                () -> new HttpMessageConverters(stringConverter, fastJsonConverter);
        return new SpringEncoder(objectFactory);
    }
}



FeignClient超时配置

Feign的调用分为两层,Ribbon的调用和Hystrix的调用。但是高版本的Hystrix默认是关闭的。一般出现想这样的异常:Read timed out executing POST http://***,是由Ribbon引起,这样可以适当得调大一下Ribbon的超时时间。

ribbon:
  ConnectTimeout: 2000
  ReadTimeout: 5000

HystrixRuntimeException: XXX timed -out and no fallback available .这就是Hystrix的超时报错

feign:
  hystrix:
    enabled: true
# 设置hystrix超时时间
hystrix:
  shareSecurityContext: true
  command:
    default:
      circuitBreaker:
        sleepWindowinMilliseconds: 10000
        forceClosed: true
      execution:
        isolation:
          thread:
            timeoutinMilliseconds: 10000



支持属性文件配置



对单个特定名称的FeignClient进行配置

@FeignClient的配置信息可以通过配置文件的方式来配置

feign:
  client:
    config:
      # 需要配置的FeignName
      github-client:
        # 连接超时时间
        connectTimout: 5000
        # 读超时时间
        readTimeut: 5000
        # Feign的日志级别
        loggerLevel: full
        # Feign的错误解码器
        errorDecode: com.example.SimpleErrorDecoder
        # 设置重试
        retryer: com.example.SimpleRetryer
        # 拦截前
        requestInterceptors:
          - com.example.FirstInterceptor
          - com.example.SecondInterceptor
        decode404: false
        # Feign的编码器
        encoder: com.example.SimpleEncoder
        # Feign的解码器
        decoder: com.example.SimpleDecoder
        # Feign的contract配置
        contract: com.example.SimpleContract



作用于所有FeignClient的配置

在@EnableFeignClients注解上有一个defaultConfiguration属性,可以将默认设置写成一个配置类,例如这个类叫做DefaultFeignClientConfiguration。

@SpringBootApplication
@EnableFeignClients(defaultConfiguration = DefaultFeignClientConfiguration.class)
public class FeignClientConfigApplication{
    SpringApplication.run(FeignClientConfigApplication.class, args);
}

同时也可以在配置文件中配置:

feign:
  client:
    config:
      default:
        # 连接超时时间
        connectTimout: 5000
        # 读超时时间
        readTimeut: 5000
        # Feign的日志级别
        loggerLevel: full
        ...

但是如果以上两种方式(在配置文件和在注解中配置FeignClient的全局配置),最后配置文件会覆盖注解上执行配置类的方式。但是可以在配置文件中添加feign.client.default-to-properties=false来改变Feigin配置的优先级。



OpenFeign实战

Feign默认是使用JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,就是利用HTTP的persistence connection.。这样可以使用其他优秀的Client去替换。这样可以设置连接池,超时时间等对服务之间的调用调优。下面介绍使用Http Client和Okhttp替换Feign默认的Client。



使用Http Client替换默认的Client

添加依赖:

<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.13</version>
</dependency>
<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-httpclient</artifactId>
	<version>11.7</version>
</dependency>

application.yml:

feign:
  httpclient:
    enabled: true

关于Http Client的一些配置也是可以在配置文件中配置的:

Spring Cloud OpenFeign介绍与使用_httpclient_02

在org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration中是关于HttpClient的配置。

当没有CloseableHttpClient这个bean的时候,就是会由这个类来生成Http Client的默认配置。所以说对于HttpClient的自定义配置可以通过自己注入CloseableHttpClient,还有HttpClientConnectionManager管理连接的bean。其实OpenFeign对HttpClient的支持很好,因为它的一些属性可以在配置文件中配置。

我们也可以在配置类中更加灵活的配置Http Client。

/**
 * http链接池 服务器返回数据(response)的时间,超过该时间抛出read timeout
 *
 * @return
 */
@Bean
public CloseableHttpClient httpClient() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
	Registry<ConnectionSocketFactory> registry =
			RegistryBuilder.<ConnectionSocketFactory>create()
					.register("http", PlainConnectionSocketFactory.getSocketFactory())
					.register("https", SSLConnectionSocketFactory.getSocketFactory())
					// 这样也可以忽略证书
					// .register("https", getNoValidSslConnectionSocketFactory())
					.build();
	PoolingHttpClientConnectionManager connectionManager =
			new PoolingHttpClientConnectionManager(registry);
	// 设置整个连接池最大连接数 根据自己的场景决定
	int maxPoolSize = Runtime.getRuntime().availableProcessors() * 2;
	connectionManager.setMaxTotal(maxPoolSize * 8);
	// 路由是对maxTotal的细分
	// 分配给同一个route(路由)最大的并发连接数
	connectionManager.setDefaultMaxPerRoute(maxPoolSize);
	RequestConfig requestConfig =
			RequestConfig.custom()
					.setSocketTimeout(15000) // 服务器返回数据(response)的时间,超过该时间抛出read timeout
					.setConnectTimeout(15000) // 连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
					.setConnectionRequestTimeout(
							3000) // 从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
					.build();
	return HttpClientBuilder.create()
			.setDefaultRequestConfig(requestConfig)
			.setConnectionManager(connectionManager)
			.build();
}

/**
 * 获取 不验证SSL的 SSL连接器
 *
 * @return
 * @throws NoSuchAlgorithmException
 * @throws KeyManagementException
 * @throws KeyStoreException
 */
private SSLConnectionSocketFactory getNoValidSslConnectionSocketFactory()
		throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
	TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> true;
	SSLContext sslContext =
			SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
	return new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
}



使用Okhttp替换默认的Client

其实和Http Client一样的配置,也是在配置文件中开启。

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-okhttp -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>11.7</version>
</dependency>

 application.yml:

feign:
  okhttp:
    enabled: true

这样开启之后,Client就被替换了。同理在org.springframework.cloud.openfeign.clientconfig包下,也有一个关于Okhttp的配置类OkHttpFeignConfiguration 。 

很明显OkHttpClient是核心功能执行的类。因为OpenFeign中有一个类FeignHttpClientProperties,有了这个类关于HttpClient的属性就可以在配置文件中设置了。但是Okhttp没有这一个类似的类,所以一般可以自己注入一个OkHttpClient去设置这些属性。

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class OkHttpConfig {

    @Bean
    public okhttp3.OkHttpClient okHttpClient() {
        return new okhttp3.OkHttpClient.Builder()
                //设置连接超时
                .connectTimeout(60, TimeUnit.SECONDS)
                //设置读超时
                .readTimeout(60, TimeUnit.SECONDS)
                //设置写超时
                .writeTimeout(60, TimeUnit.SECONDS)
                //是否自动重连
                .retryOnConnectionFailure(true)
                .connectionPool(new ConnectionPool())
                //构建OkHttpClient对象
                .build();
    }
}

关于自定义OkHttpClient的配置,可以参考OpenFeign里OkHttpFeignConfiguration的配置,例如ConnectionPool这个bean。



文件上传

想要实现文件上传功能,需要编写Encoder去实现文件上传。现在OpenFeign提供了子项目feign-form。

(1)服务提供者

添加方法:

//POST http://localhost:8001/provider/uploadFile
//以表单上传文件 Content-Type multipart/form-data
@RequestMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String fileUploadServer(MultipartFile file) throws Exception{
	return file.getOriginalFilename();
}

(2)服务消费者

添加依赖:

<!-- Feign文件上传依赖-->
<dependency>
	<groupId>io.github.openfeign.form</groupId>
	<artifactId>feign-form</artifactId>
	<version>3.8.0</version>
</dependency>

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

在feign配置类中添加解码器:

@Bean
@Primary
@Scope("prototype")
public Encoder multipartFormEncoder() {
	return new SpringFormEncoder();
}

FeignClient接口添加方法:

8/***
 * 1.produces,consumes必填
 * 2.注意区 分@RequestPart和RequestParam,不要将
 * @RequestPart(value = "file") 写成@RequestParam(value = "file")
 * @param file
 * @return
 */
@RequestMapping(value = "/provider/uploadFile",
		produces = {MediaType.APPLICATION_JSON_VALUE},
		consumes = MediaType.MULTIPART_FORM_DATA_VALUE, method = RequestMethod.POST)
String fileUpload(@RequestPart(value = "file") MultipartFile file);

Controller添加方法:

//文件上传
//POST http://localhost:9901/consumer/upload
//以表单上传文件 Content-Type multipart/form-data
@RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String imageUpload(MultipartFile file) throws Exception {
	return providerClient.fileUpload(file);
}



Feign服务调用请求方式及参数总结

无参情况

无参情况就是说我们的方法内不接收参数。

(1)GET请求

当我们只写RequestMapping,而不指定RequestMethod的时候。默认的method为一个get请求。

@RequestMapping("/noArgs/getDemo")
public void noArgsGetDemo();

(2)POST请求

@RequestMapping(value = "/noArgs/postDemo",method = RequestMethod.POST)
public void noArgsPostDemo();

也可以直接使用PostMapping

@PostMapping(value = "/noArgs/postDemo")
public void noArgsPostDemo();

单个参数的情况

方法内只有一个参数。

(1)GET请求

get请求方式接参,只能使用RequestParam注解

@RequestMapping(value = "/singleArg/getDemo")
public void singleArgGetDemo(@RequestParam String name);

不写RequestMethod注解,默认就是get请求。

(2)POST请求

post请求方式接参,可以使用三种方式,一种是不写,一种是RequestParam,一种是RequestBody。

① RequestParam

先说说RequestParam这种方式。需要指明method,如果不指明则和上方一样了。默认是get。

@RequestMapping(value = "/singleArg/PostDemo",method = RequestMethod.POST)
public void singleArgPostDemo(@RequestParam String name);

② RequestBody

一旦使用RequestBody这种方式,它就是post请求,不用写method了。

@RequestMapping(value = "/singleArg/PostDemo")
public void singleArgPostDemo(@RequestBody String name);

这个注解就很强势了,你写post,不写或者写get都没用,不会生效的,只要有这个方式那它就是Post请求了。

③ 什么也不写

@RequestMapping(value = "/singleArg/PostDemo")
public void singleArgPostDemo(String name);

此时默认会在参数前加上RequestBody注解,然后就会变成Post请求。

多参

(1)GET请求

多个参数也是使用@RequestParam注解。

@RequestMapping(value = "/moreArgs/getDemo")
public void moreArgGetDemo(@RequestParam String name,@RequestParam String sex);

使用了RequestParam注解,默认method就是get。

(2)post请求

多个参数只能有一个是requestBody方式,其他应该使用requestParam方式。

@RequestMapping(value = "/moreArgs/postDemo")
public void moreArgPostDemo(@RequestBody String name,@RequestParam String sex);

也可以全部使用RequestParam方式,但是要指定post。

@RequestMapping(value = "/moreArgs/postDemo",method = RequestMethod.POST)
public void moreArgPostDemo(@RequestParam String name,@RequestParam String sex);

如果要是参数前,都没写注解,则会报错,因为会默认加上两个RequestBody。



SpringMVC契约和Feign契约

使用Feign的时候遇到以下错误:

Method search not annotated with HTTP method type (ex. GET, POST)
  • SpringMVC 契约:@PostMapping 、@GetMapping、@RequestMapping 等等,传参使用 @RequestParam 、@RequestBody。
  • @RequestLine,传参使用 @Param  。
//多个参数 SpringMVC契约和Feign契约的写法
// @RequestMapping(value = "/provider/helloMultiParamForBody", method = RequestMethod.POST)
@RequestLine("POST /provider/helloMultiParamForBody")
@Headers({"content-type: application/json;charset=utf-8"})
String helloMultiParamForBody(@RequestBody Map<String, Object> userInfo);

默认是使用SpringMVC契约,是在FeignClientsConfiguration定义的:

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
	return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}

如果想用Feign契约替换SpringMVC契约,可以这样做,在FeignClient的配置类上定义:

@Bean
public Contract feignContract(){
	return new Contract.Default();
}

如果想了解@RequestMapping的用法,请参考:Spring MVC的@RequestMapping注解



旧的已经老的Feign契约和使用:

1.准备config

@Configuration
public class FeginConfig {
    @Bean
    public Contract feignConfiguration() {
        return new feign.Contract.Default();
    }
}

2.申明接口

@FeignClient(name = "EMPLOYEE", configuration = FeginConfig.class)
public interface EmployeeService1 {
    @RequestLine("POST /employee/add")
    @Headers({"Content-Type: application/json", "Accept: application/json"})
    CommonResult add(TblEmployee employee);

    @RequestLine("POST /employee/addBatch")
    @Headers({"Content-Type: application/json", "Accept: application/json"})
    CommonResult addBatch(List<TblEmployee> employee);
}



新的使用RequestMapping/PostMapping/GetMapping注解

1.此种方式比较简单,直接定义接口

@FeignClient("EMPLOYEE")
public interface EmployeeService {
    //@PostMapping(value = "/employee/add")
    @RequestMapping(value="/employee/add",consumes = "application/json",method = RequestMethod.POST)
    CommonResult add(TblEmployee employee);

    @PostMapping(value = "/employee/addBatch")
        //@RequestMapping(value="/employee/addBatch",consumes = "application/json",method = RequestMethod.POST)
    CommonResult addBatch(List<TblEmployee> employee);
}

2.使用

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient  

@EnableFeignClients
@RestController
public class WebApp {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @Autowired(required = false)
    private Contract cc;

    @Autowired(required = false)
    private EmployeeService employeeService;

    @Autowired(required = false)
    private EmployeeService1 employeeService1;

    @PostMapping("add")
    public CommonResult add(@RequestBody TblEmployee employee) {
        return employeeService.add(employee);
    }

    @PostMapping("addBatch")
    public CommonResult addBatch(@RequestBody List<TblEmployee> employees) {
        return employeeService.addBatch(employees);
    }

    @GetMapping("/test")
    public String test() {
        Class<? extends Contract> aClass = cc.getClass();
        return aClass == null ? "" : aClass.getName();
    }
}

分别对应上面2中使用qi'y

 



解决首次请求失败问题

由于OpenFeign整合了Ribbon和Hystrix,可能会出现首次调用失败的问题。

主要原因是:Hystrix默认的超时时间是1秒,如果超过这个时间没有响应,就会进入fallback代码。由于Bean的装配和懒加载的机制,Feign首次请求都会比较慢。如此一来当响应时间大于1秒就会进入fallback而导致请求失败。解决方法:

(1)将Hystrix的超时时间调大,此方法比较好

hystrix:
  command:
    default:
   execution:
        isolation:
          thread:
            timeoutInMillseconds: 5000 # 5秒

可以查看spring-cloud-starter-openfeign中的HystrixCommandProperties。

注意:关于hystrix在application.properties配置是没提示的,但是HystrixCommandProperties是会获取的。

(2)禁用Hystrix的超时时间

hystrix:
  command:
    default:
      execution:
        timout:
          enable: false

(3)使用Feign的时候关闭Hystrix,这是不推荐的

feign:
  hystrix:
    enable: false



返回图片流的处理方式

对于返回的是图片,一般都是字节数组。但是Controller不能直接返回byte,所以被调用的API返回的类型应该使用Response(feign.Response)。

以生成一个二维码为例。

(1)服务提供者

添加新的依赖,使用hutool快速生成二维码。

<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.6.3</version>
</dependency>
<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>core</artifactId>
	<version>3.3.3</version>
</dependency>

controller的接口,这里仅简单的生成了一个二维码,二维码还可以添加更加多的信息。这里就不详细介绍,hutool的QrCodeUtil有很多方法,有兴趣的可以自行研究。

@GetMapping(value = "/qrcode")
public byte[] image() {
	return generateQrCode();
}
/**
 * 先简单的生成一个url的二维码,指向百度
 * @return
 */
private byte[] generateQrCode() {
	return QrCodeUtil.generatePng("https://www.baidu.cn/", 300, 300);
}

(2)服务消费者

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

feignclient添加新接口

//这里使用的是feign.Response
@RequestMapping(value = "/provider/qrcode", method = RequestMethod.GET)
Response getQrCode();

ontroller的修改,对于要在前端页面显示图片,一般用的最多的是返回页面一个url,但是这都是存储好的图片,但是每次生成验证码和二维码这些,服务端可能并不会存储起来。所以并不能返回一个url地址,对于验证码用的返回前端Base64编码。二维码的话可以基于HttpServletResponse,produces返回字节流和Base64图片。

这里使用HttpServletResponse,添加方法:

@GetMapping("/qrcode")
public void getQrCode(HttpServletResponse response) {
	Response res = fileUploadApiService.getQrCode();
	try {
		InputStream inputStream = res.body().asInputStream();
		response.setContentType(MediaType.IMAGE_PNG_VALUE);
		IOUtils.copy(inputStream,response.getOutputStream());
	} catch (IOException e) {
		e.printStackTrace();
	}
}

因为是GET请求,我们可以在浏览器输入:http://localhost:9901/consumer/qrcode访问。



调用传递token

正常的来说,系统都是有认证鉴权的功能,不管是JWT还是security,在外部请求到A服务时,是带有token过来的,但是此请求在A服务内部通过Feign调用B服务时,就会发生token的丢失。

解决方法也是不难,就是在使用Feign远程调用时,在请求头里携带一下token,一般token是放在请求头里面。

Feign提供的拦截器RequestInterceptor,这样可以拦截Feign的请求,在请求头里添加token。

@Component
public class FeignTokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        if (null == getHttpServletRequest()) {
            //此处可以记录一些日志
            return;
        }
        //将获取Token对应的值往下面传
        requestTemplate.header("token", getHeaders(getHttpServletRequest()).get("token"));
    }

    private HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Feign拦截器拦截请求获取Token对应的值
     *
     * @param request
     * @return
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String key = enumeration.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }
}

 

记得要在feign的配置类中应用@Bean才起作用。