乐趣区

关于java:Spring-Cloud-OpenFeign入门和实战

文章首发:Spring Cloud OpenFeign 入门和实战

OpenFeign 是什么

Feign 是一个申明式的 Web Service 客户端,是一种申明式、模板化的 HTTP 客户端。而 OpenFeign 是 Spring Cloud 在 Feign 的根底上反对了 Spring MVC 的注解,如 @RequesMapping 等等。
OpenFeign 的 @FeignClient 能够解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动静代理的形式产生实现类,实现类中做负载平衡并调用其余服务。
Feign 能够把 Rest 的申请进行暗藏,伪装成相似 SpringMVC 的 Controller 一样。你不必再本人拼接 url,拼接参数等等操作,所有都交给 Feign 去做。

  1. 可插拔的注解反对,包含 Feign 注解和 JSX-RS 注解
  2. 反对可插拔的 HTTP 编码器和解码器
  3. 反对 Hystrix 和它的 Fallback
  4. 反对 Ribbon 的负载平衡
  5. 反对 HTTP 申请和响应的压缩。

OpenFeign 入门

创立父 Pom 工程:cloud-openfeign-practice

此工程用于寄存所有对于 openfeign 的示例。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.msr.better</groupId>
    <artifactId>cloud-openfeign-practice</artifactId>
    <version>1.0</version>

    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring.cloud-version>Hoxton.SR3</spring.cloud-version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

创立模块:cloud-openfeign-hehllo

pom.xml

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

配置文件

application.xml

server:
  port: 8010
spring:
  application:
    name: openfeign-hello
# 日志
logging:
  level:
    com.msr.better.feign.service.HelloFeignService: debug

配置类

@Configuration
public class HelloFeignServiceConfig {

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

serivce

@FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)
public interface HelloFeignService {

    /**
     * content:
     * {
     *  "message":"Validation Failed",
     *  "errors":[{"resource":"Search","field":"q","code":"missing"}],
     *  "documentation_url":"https://developer.github.com/v3/search"
     *  }
     *
     * @param queryStr
     * @return
     */
    @GetMapping(value = "/search/repositories")
    String searchRepo(@RequestParam("q") String queryStr);

}

在下面的 HelloFeignService 中通过 @FeignClient 注解手动指定了该接口要拜访的 URL(https://api.github.com),调用 …

controller

@RestController
public class HelloFeignController {

    @Autowired
    private HelloFeignService helloFeignService;

    @GetMapping(value = "/search/github")
    public String searchGithubRepoByStr(@RequestParam("searchStr") String searchStr) {return helloFeignService.searchRepo(searchStr);
    }
}

启动类

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

@EnableFeignClients 包扫描时,扫描所有 @FeignClient。

启动测试

运行启动类之后,在浏览器或者 PostMan 之类的工具拜访 http://localhost:8010/search/…

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 {...}

从 FeignClient 的注解能够看得出,ElementType.TYPE阐明 FeignClient 的作用指标是接口。其罕用的属性如下:

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

OpenFeign 开始 GZIP 压缩

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

server:
  port: 8011
spring:
  application:
    name: openfeign-gzip
logging:
  level:
    com.msr.better.feign.service.HelloFeignService: debug
feign:
  # 压缩配置
  compression:
    request:
      enabled: true
      # 配置压缩反对的 MIME TYPE
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048  # 配置压缩数据大小的上限
    response:
      enabled: true # 配置响应 GZIP 压缩

等价的 properties 配置

feign.compression.request.enabled=true
# 配置压缩反对的 MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的上限
feign.compression.request.min-request-size=2048
# 配置响应 GZIP 压缩
feign.compression.response.enabled=true

反对属性文件配置

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

@FeignClientde 的配置信息能够通过配置文件的形式来配置

server:
  port: 8011
spring:
  application:
    name: openfeign-gzip
logging:
  level:
    com.msr.better.feign.service.HelloFeignService: debug
feign:
  # 压缩配置
  compression:
    request:
      enabled: true
      # 配置压缩反对的 MIME TYPE
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048  # 配置压缩数据大小的上限
    response:
      enabled: true # 配置响应 GZIP 压缩
  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 配置的优先级。

FeignClient 开启日志

其实在下面的就曾经是配置了 FeignClient 的日志了。Feign 为每一个 Feign 都提供了一个 fegin.Logger 实例。能够在配置中开启日志输入,开启的步骤也很简略。

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

logging:
  level:
    # 指定那个 FeignClient 接口的申请须要输入日志,以及日志级别
    com.msr.better.feign.service.HelloFeignService: debug

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


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

又或者通过配置类配置,并在 @FeignClient 注解中执行改配置类。

@Configuration
public class HelloFeignServiceConfig {

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

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

OpenFeign 实战

替换默认的 Client

Feign 默认是应用 JDK 原生的 URLConnection 发送 HTTP 申请,没有连接池,然而对每个地址会放弃一个长连贯,就是利用 HTTP 的 persistence connection.。这样能够应用其余优良的 Client 去替换。这样能够设置连接池,超时工夫等对服务之间的调用调优。上面介绍应用 Http Client 和 Okhttp 替换 Feign 默认的 Client。步骤也很简略。

应用 Http Client 替换默认的 Client

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Cloud OpenFeign 的 Starter 的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 应用 Apache HttpClient 替换 Feign 原生 httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 8010
spring:
  application:
    name: openfeign-httpclient
feign:
  httpclient:
    enabled: true

对于 Http Client 的一些配置也是能够在配置文件中配置的

org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration 中是对于 HttpClient 的配置:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({CloseableHttpClient.class})
public class HttpClientFeignConfiguration {private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
    private CloseableHttpClient httpClient;
    @Autowired(required = false)
    private RegistryBuilder registryBuilder;

    public HttpClientFeignConfiguration() {}

    @Bean
    @ConditionalOnMissingBean({HttpClientConnectionManager.class})
    public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
        this.connectionManagerTimer.schedule(new TimerTask() {public void run() {connectionManager.closeExpiredConnections();
            }
        }, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
        return connectionManager;
    }

    @Bean
    @ConditionalOnProperty(value = {"feign.compression.response.enabled"},
        havingValue = "true"
    )
    public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties();
        this.httpClient = this.createClient(builder, httpClientConnectionManager, httpClientProperties);
        return this.httpClient;
    }

    @Bean
    @ConditionalOnProperty(value = {"feign.compression.response.enabled"},
        havingValue = "false",
        matchIfMissing = true
    )
    public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {this.httpClient = this.createClient(httpClientFactory.createBuilder(), httpClientConnectionManager, httpClientProperties);
        return this.httpClient;
    }

    private CloseableHttpClient createClient(HttpClientBuilder builder, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
        CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig).setConnectionManager(httpClientConnectionManager).build();
        return httpClient;
    }

    @PreDestroy
    public void destroy() throws Exception {this.connectionManagerTimer.cancel();
        if (this.httpClient != null) {this.httpClient.close();
        }

    }
}

很显著当没有 CloseableHttpClient 这个 bean 的时候,就是会由这个类来生成 Http Client 的默认配置。所以说对于 HttpClient 的自定义配置能够通过本人注入 CloseableHttpClient。还有HttpClientConnectionManager 治理连贯的 bean。其实 OpenFeign 对 HttpClient 的反对很好,因为它的一些属性能够在配置文件中配置。

应用 Okhttp 替换默认的 Client

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

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Cloud OpenFeign 的 Starter 的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 8011
spring:
  application:
    name: openfeign-okhttp
feign:
  okhttp:
    enabled: true
# 日志
logging:
  level:
    com.msr.better.feign.service.HelloFeignService: debug

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

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({OkHttpClient.class})
public class OkHttpFeignConfiguration {
    private OkHttpClient okHttpClient;

    public OkHttpFeignConfiguration() {}

    @Bean
    @ConditionalOnMissingBean({ConnectionPool.class})
    public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {Integer maxTotalConnections = httpClientProperties.getMaxConnections();
        Long timeToLive = httpClientProperties.getTimeToLive();
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    }

    @Bean
    public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {Boolean followRedirects = httpClientProperties.isFollowRedirects();
        Integer connectTimeout = httpClientProperties.getConnectionTimeout();
        this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build();
        return this.okHttpClient;
    }

    @PreDestroy
    public void destroy() {if (this.okHttpClient != null) {this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
        }

    }
}

很显著 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。

Post 和 Get 的多参数传递

在应用 OpenFeign 实现服务之间的调用时,很多时候是要传递多个参数。

创立 cloud-openfeign-eureka-server 模块

Eureka Server 注册核心

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- springboot web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 不必 Tomcat, 应用 undertow -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>io.undertow</groupId>
            <artifactId>undertow-servlet</artifactId>
        </dependency>
    </dependencies>

配置文件 application.yml

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  server :
    enable-self-preservation: false
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

启动类

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

创立 cloud-openfeign-provider 模块

服务提提供者

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

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

配置文件 application.yml

server:
  port: 8012
spring:
  application:
    name: openfeign-provider
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  #eureka.instance.prefer-ip-address  示意将本人的 IP 注册到 Eureka Server 上,#如果不配置, 会将以后服务提供者所在的主机名注册到 Eureka Server 上。instance:
    prefer-ip-address: true

实体类和控制器

public class Order {
    private Long id;
    private String name;
    private int age;

    public Long getId() {return id;}

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

    public String getName() {return name;}

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

    public int getAge() {return age;}

    public void setAge(int age) {this.age = age;}
}
@RestController
@RequestMapping("/order")
public class OrderController {@GetMapping(value = "/add")
    public String addUser(Order order, HttpServletRequest request) {String token = request.getHeader("oauthToken");
        return "hello," + order.getName();}

    @PostMapping(value = "/update")
    public String updateUser(@RequestBody Order order) {return "hello," + order.getName();
    }
}

启动类

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

创立 cloud-openfeign-consumer 模块

消费者服务

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 应用 Apache HttpClient 替换 Feign 原生 httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
    </dependencies>

配置文件 application.yml

server:
  port: 8011
spring:
  application:
    name: openfeign-consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
feign:
  httpclient:
    enabled: true

实体类

package com.msr.better.feign.model;

public class Order {
    private Long id;
    private String name;
    private int nums;
    // 此处省略了 getter 和 setter
}

FeignClient 接口

@FeignClient("openfeign-provider")
public interface OrderApiService {@GetMapping(value = "/order/add")
    String addUser(@SpringQueryMap Order order);

    @PostMapping(value = "/order/update")
    String updateUser(@RequestBody Order order);
}

此处的 Client 接口中对于 GET 申请传递实体类应用了注解 @SpringQueryMap。OpenFeign@QueryMap 批注反对将 POJO 用作 GET 参数映射。然而默认的 OpenFeign QueryMap 正文与 Spring 不兼容,因为它短少 value 属性。

Spring Cloud OpenFeign 提供了等效的 @SpringQueryMap 正文,该正文用于将 POJO 或 Map 参数正文为查问参数映射。

在一些材料中说什么 OpenFeign 的什么 GET 不能传递 POJO,写了个拦截器把实体类转换了,预计是 OpenFeign 的版本低,在新的 OpenFeign 中是有了对 QueryMap 的反对了。

配置类

@Configuration
public class CoreAutoConfiguration {

    @Autowired
    private HttpClient httpClient;

    @Bean
    public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setHttpClient(httpClient);
        factory.setReadTimeout(3000);
        factory.setConnectTimeout(3000);
        factory.setConnectionRequestTimeout(3000);
        return factory;
    }

    /**
     * {@link RestTemplate}的 setRequestFactory 办法反对 HttpClient 和 Okhttp 等 Client
     * 默认是应用{@link SimpleClientHttpRequestFactory} Http 的申请是应用原生的 URLConnection
     *
     * @return RestTemplate 的 bean
     */
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(httpComponentsClientHttpRequestFactory());
        return restTemplate;
    }
}

下面是替换了 RestTemplate 的 Client。因为 RestTemplate 默认是应用 URLConnection。这里是应用 HttpClient 替换了。

控制器

@RestController
@RequestMapping("api")
public class OrderController {

    @Autowired
    private OrderApiService orderApiService;

    /**
     * @param order
     * @return
     */
    @PostMapping("/get/pojo")
    public String getPojo(@RequestBody Order order) {return orderApiService.addUser(order);
    }

    @PostMapping("/post/pojo")
    String postPojo(@RequestBody Order order){return orderApiService.updateUser(order);
    }
}

最初就能够测试 http://localhost:8011/get/poj…

文件上传

持续应用上一节创立的 Eureka Server。而后创立一下两个模块用作文件上传。

想要实现文件上传性能,须要编写 Encoder 去实现文件上传。当初 OpenFeign 提供了子项目 feign-form(https://github.com/OpenFeign/…

创立 cloud-openfeign-fileupload-server

文件上传接口的提供者

    <dependencies>
        <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>
    </dependencies>

配置文件 application.yml

server:
  port: 8012
spring:
  application:
    name: openfeign-file-server

eureka:
  server:
    enableSelfPreservation: false
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

启动类

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

上传接口

@RestController
public class FileController {@PostMapping(value = "/uploadFile/server", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUploadServer(MultipartFile file) throws Exception{return file.getOriginalFilename();
    }
}

创立 cloud-openfeign-fileupload-client

文件上传接口的调用者

    <dependencies>
        <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 Cloud OpenFeign 的 Starter 的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 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>
    </dependencies>

配置文件 application.yml

server:
  port: 8011
spring:
  application:
    name: openfeign-upload-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

配置类

@Configuration
public class FeignMultipartSupportConfig {

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

控制器

@RestController
@RequestMapping("file")
public class FeignUploadController {

    @Autowired
    private FileUploadApiService fileUploadApiService;

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String imageUpload(MultipartFile file) throws Exception {return fileUploadApiService.fileUpload(file);
    }
}

FeignClient

@FeignClient(value = "openfeign-file-server", configuration = FeignMultipartSupportConfig.class)
public interface FileUploadApiService {
    /***
     * 1.produces,consumes 必填
     * 2. 留神辨别 @RequestPart 和 RequestParam,不要将
     * @RequestPart(value = "file") 写成 @RequestParam(value = "file")
     * @param file
     * @return
     */
    @PostMapping(value = "/uploadFile/server",
            produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String fileUpload(@RequestPart(value = "file") MultipartFile file);
}

测试

运行 Eureka Server、cloud-openfeign-fileupload-client 模块和 cloud-openfeign-fileupload-server 模块,应用 PostMan 进行测试。最初胜利返回文件的名字,文件胜利的上传到 server 上了。

解决首次申请失败问题

因为 OpenFeign 整合了 Ribbon 和 Hystrix,可能会呈现首次调用失败的问题。

次要起因是:Hystrix 默认的超时工夫是 1 秒,如果超过这个工夫没有响应,就会进入 fallback 代码。因为 Bean 的拆卸和懒加载的机制,Feign 首次申请都会比较慢。如此一来当响应工夫大于 1 秒就会进入 fallback 而导致申请失败。解决办法:

  1. 将 Hystrix 的超时工夫调大,此办法比拟好

    hystrix:
      command:
        default:
       execution:
            isolation:
              thread:
                timeoutInMillseconds: 5000 # 5 秒
  2. 禁用 Hystrix 的超时工夫

    hystrix:
      command:
        default:
          execution:
            timout:
              enable: false
  3. 应用 Feign 的时候敞开 Hystrix,这是不举荐的

    feign:
      hystrix:
        enable: false

返回图片流的解决形式

对于返回的是图片,个别都是字节数组。然而 Contrller 不能间接返回 byte,所以被调用的 API 返回的类型应该应用 Response。

应用下面的文件上传创立的模块中增加一个返回图片的接口。以生成一个二维码为例。

cloud-openfeign-fileupload-server 的一些批改

增加新的依赖,应用 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>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-core</artifactId>
        </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);
    }

cloud-openfeign-fileupload-client 的一些批改

增加新依赖

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

feignclient 增加新接口

    @GetMapping("/qrcode")
    Response getQrCode();

controller 的批改,对于要在前端页面显示图片,个别用的最多的是返回页面一个 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();
        }
    }

浏览器拜访:http://localhost:8011/file/qr…

调用传递 token

失常的来说,零碎都是有认证鉴权的性能,不论是 JWT 还是 security,在内部申请到 A 服务时,是带有 token 过去的,然而此申请在 A 服务外部通过 Feign 调用 B 服务时,就会产生 token 的失落。

解决办法也是不难,就是在应用 Feign 近程调用时,在申请头里携带一下 token,个别 token 是放在申请头外面。

Feign 提供的拦截器RequestInterceptor,这样能够拦挡 Feign 的申请,在申请头里增加 token。对于这部分代码,在 cloud-openfeign-consumer 和 cloud-openfeign-provider 上进行增加。

批改 cloud-openfeign-provider

批改一下办法,便于展现后果

    @PostMapping(value = "/update")
    public String updateOrder(@RequestBody Order order, HttpServletRequest request) {String token = request.getHeader("token");
        return "hello," + order.getName() + "" +"haha!I get a token: " + token;}

批改 cloud-openfeign-consumer

增加拦截器实现feign.RequestInterceptor

@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 的用法,后续如果有对于 Feign 新的货色将会新开文章述说。

退出移动版