项目作者: MaiSR9527

项目描述 :
Spring Cloud OpenFeign入门和实战
高级语言: Java
项目地址: git://github.com/MaiSR9527/cloud-openfeign-practice.git
创建时间: 2021-04-07T15:57:56Z
项目社区:https://github.com/MaiSR9527/cloud-openfeign-practice

开源协议:

下载


文章首发: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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>com.msr.better</groupId>
  7. <artifactId>cloud-openfeign-practice</artifactId>
  8. <version>1.0</version>
  9. <packaging>pom</packaging>
  10. <parent>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-parent</artifactId>
  13. <version>2.2.3.RELEASE</version>
  14. <relativePath></relativePath> <!-- lookup parent from repository -->
  15. </parent>
  16. <properties>
  17. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  18. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  19. <java.version>1.8</java.version>
  20. <spring.cloud-version>Hoxton.SR3</spring.cloud-version>
  21. </properties>
  22. <dependencyManagement>
  23. <dependencies>
  24. <dependency>
  25. <groupId>org.springframework.cloud</groupId>
  26. <artifactId>spring-cloud-dependencies</artifactId>
  27. <version>${spring.cloud-version}</version>
  28. <type>pom</type>
  29. <scope>import</scope>
  30. </dependency>
  31. </dependencies>
  32. </dependencyManagement>
  33. <dependencies>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-web</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-starter-actuator</artifactId>
  41. </dependency>
  42. </dependencies>
  43. <build>
  44. <plugins>
  45. <plugin>
  46. <groupId>org.springframework.boot</groupId>
  47. <artifactId>spring-boot-maven-plugin</artifactId>
  48. </plugin>
  49. </plugins>
  50. </build>
  51. </project>

创建模块:cloud-openfeign-hehllo

pom.xml

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-openfeign</artifactId>
  9. </dependency>
  10. </dependencies>

配置文件

application.xml

  1. server:
  2. port: 8010
  3. spring:
  4. application:
  5. name: openfeign-hello
  6. # 日志
  7. logging:
  8. level:
  9. com.msr.better.feign.service.HelloFeignService: debug

配置类

  1. @Configuration
  2. public class HelloFeignServiceConfig {
  3. /**
  4. * Logger.Level 的具体级别如下:
  5. * NONE:不记录任何信息
  6. * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
  7. * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
  8. * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
  9. */
  10. @Bean
  11. Logger.Level feignLoggerLevel() {
  12. return Logger.Level.FULL;
  13. }
  14. }

serivce

  1. @FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)
  2. public interface HelloFeignService {
  3. /**
  4. * content:
  5. * {
  6. * "message":"Validation Failed",
  7. * "errors":[{"resource":"Search","field":"q","code":"missing"}],
  8. * "documentation_url":"https://developer.github.com/v3/search"
  9. * }
  10. *
  11. * @param queryStr
  12. * @return
  13. */
  14. @GetMapping(value = "/search/repositories")
  15. String searchRepo(@RequestParam("q") String queryStr);
  16. }

在上面的HelloFeignService中通过@FeignClient注解手动指定了该接口要访问的URL(https://api.github.com),调用searchGithubRepoByStr方法时,最终会发起GET请求https://api.github.com/search/repositories?q=输入的参数。

controller

  1. @RestController
  2. public class HelloFeignController {
  3. @Autowired
  4. private HelloFeignService helloFeignService;
  5. @GetMapping(value = "/search/github")
  6. public String searchGithubRepoByStr(@RequestParam("searchStr") String searchStr) {
  7. return helloFeignService.searchRepo(searchStr);
  8. }
  9. }

启动类

  1. @SpringBootApplication
  2. @EnableFeignClients
  3. public class OpenFeignHelloApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(OpenFeignHelloApplication.class, args);
  6. }
  7. }

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

启动测试

运行启动类之后,在浏览器或者PostMan之类的工具访问http://localhost:8010/search/github?searchStr=spring-cloud

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注解

  1. @Target({ElementType.TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. 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压缩,以此来提供通信效率。只需在配置文件中配置即可,比较简单。

  1. server:
  2. port: 8011
  3. spring:
  4. application:
  5. name: openfeign-gzip
  6. logging:
  7. level:
  8. com.msr.better.feign.service.HelloFeignService: debug
  9. feign:
  10. # 压缩配置
  11. compression:
  12. request:
  13. enabled: true
  14. # 配置压缩支持的MIME TYPE
  15. mime-types: text/xml,application/xml,application/json
  16. min-request-size: 2048 # 配置压缩数据大小的下限
  17. response:
  18. enabled: true # 配置响应GZIP压缩

等价的properties配置

  1. feign.compression.request.enabled=true
  2. # 配置压缩支持的MIME TYPE
  3. feign.compression.request.mime-types=text/xml,application/xml,application/json
  4. # 配置压缩数据大小的下限
  5. feign.compression.request.min-request-size=2048
  6. # 配置响应GZIP压缩
  7. feign.compression.response.enabled=true

支持属性文件配置

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

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

  1. server:
  2. port: 8011
  3. spring:
  4. application:
  5. name: openfeign-gzip
  6. logging:
  7. level:
  8. com.msr.better.feign.service.HelloFeignService: debug
  9. feign:
  10. # 压缩配置
  11. compression:
  12. request:
  13. enabled: true
  14. # 配置压缩支持的MIME TYPE
  15. mime-types: text/xml,application/xml,application/json
  16. min-request-size: 2048 # 配置压缩数据大小的下限
  17. response:
  18. enabled: true # 配置响应GZIP压缩
  19. client:
  20. config:
  21. # 需要配置的FeignName
  22. github-client:
  23. # 连接超时时间
  24. connectTimout: 5000
  25. # 读超时时间
  26. readTimeut: 5000
  27. # Feign的日志级别
  28. loggerLevel: full
  29. # Feign的错误解码器
  30. errorDecode: com.example.SimpleErrorDecoder
  31. # 设置重试
  32. retryer: com.example.SimpleRetryer
  33. # 拦截前
  34. requestInterceptors:
  35. - com.example.FirstInterceptor
  36. - com.example.SecondInterceptor
  37. decode404: false
  38. # Feign的编码器
  39. encoder: com.example.SimpleEncoder
  40. # Feign的解码器
  41. decoder: com.example.SimpleDecoder
  42. # Feign的contract配置
  43. contract: com.example.SimpleContract

作用于所有FeignClient的配置

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

  1. @SpringBootApplication
  2. @EnableFeignClients(defaultConfiguration = DefaultFeignClientConfiguration.class)
  3. public class FeignClientConfigApplication{
  4. SpringApplication.run(FeignClientConfigApplication.class, args);
  5. }

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

  1. feign:
  2. client:
  3. config:
  4. default:
  5. # 连接超时时间
  6. connectTimout: 5000
  7. # 读超时时间
  8. readTimeut: 5000
  9. # Feign的日志级别
  10. loggerLevel: full
  11. ...

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

FeignClient开启日志

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

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

  1. logging:
  2. level:
  3. # 指定那个FeignClient接口的请求需要输出日志,以及日志级别
  4. com.msr.better.feign.service.HelloFeignService: debug

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

  1. @Bean
  2. Logger.Level feignLoggerLevel() {
  3. return Logger.Level.FULL;
  4. }

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

  1. @Configuration
  2. public class HelloFeignServiceConfig {
  3. /**
  4. * Logger.Level 的具体级别如下:
  5. * NONE:不记录任何信息
  6. * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
  7. * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
  8. * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
  9. */
  10. @Bean
  11. Logger.Level feignLoggerLevel() {
  12. return Logger.Level.FULL;
  13. }
  14. }

FeignClient超时配置

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

  1. ribbon:
  2. ConnectTimeout: 2000
  3. ReadTimeout: 5000

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

  1. feign:
  2. hystrix:
  3. enabled: true
  4. # 设置hystrix超时时间
  5. hystrix:
  6. shareSecurityContext: true
  7. command:
  8. default:
  9. circuitBreaker:
  10. sleepWindowinMilliseconds: 10000
  11. forceClosed: true
  12. execution:
  13. isolation:
  14. thread:
  15. timeoutinMilliseconds: 10000

OpenFeign实战

替换默认的Client

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

使用Http Client替换默认的Client

pom.xml

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <!-- Spring Cloud OpenFeign的Starter的依赖 -->
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-openfeign</artifactId>
  10. </dependency>
  11. <!-- 使用Apache HttpClient替换Feign原生httpclient -->
  12. <dependency>
  13. <groupId>org.apache.httpcomponents</groupId>
  14. <artifactId>httpclient</artifactId>
  15. </dependency>
  16. <dependency>
  17. <groupId>io.github.openfeign</groupId>
  18. <artifactId>feign-httpclient</artifactId>
  19. </dependency>
  20. </dependencies>

application.yml

  1. server:
  2. port: 8010
  3. spring:
  4. application:
  5. name: openfeign-httpclient
  6. feign:
  7. httpclient:
  8. enabled: true

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

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

  1. @Configuration(
  2. proxyBeanMethods = false
  3. )
  4. @ConditionalOnMissingBean({CloseableHttpClient.class})
  5. public class HttpClientFeignConfiguration {
  6. private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
  7. private CloseableHttpClient httpClient;
  8. @Autowired(
  9. required = false
  10. )
  11. private RegistryBuilder registryBuilder;
  12. public HttpClientFeignConfiguration() {
  13. }
  14. @Bean
  15. @ConditionalOnMissingBean({HttpClientConnectionManager.class})
  16. public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
  17. final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
  18. this.connectionManagerTimer.schedule(new TimerTask() {
  19. public void run() {
  20. connectionManager.closeExpiredConnections();
  21. }
  22. }, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
  23. return connectionManager;
  24. }
  25. @Bean
  26. @ConditionalOnProperty(
  27. value = {"feign.compression.response.enabled"},
  28. havingValue = "true"
  29. )
  30. public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
  31. HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties();
  32. this.httpClient = this.createClient(builder, httpClientConnectionManager, httpClientProperties);
  33. return this.httpClient;
  34. }
  35. @Bean
  36. @ConditionalOnProperty(
  37. value = {"feign.compression.response.enabled"},
  38. havingValue = "false",
  39. matchIfMissing = true
  40. )
  41. public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
  42. this.httpClient = this.createClient(httpClientFactory.createBuilder(), httpClientConnectionManager, httpClientProperties);
  43. return this.httpClient;
  44. }
  45. private CloseableHttpClient createClient(HttpClientBuilder builder, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
  46. RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
  47. CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig).setConnectionManager(httpClientConnectionManager).build();
  48. return httpClient;
  49. }
  50. @PreDestroy
  51. public void destroy() throws Exception {
  52. this.connectionManagerTimer.cancel();
  53. if (this.httpClient != null) {
  54. this.httpClient.close();
  55. }
  56. }
  57. }

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

使用Okhttp替换默认的Client

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

pom.xml

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <!-- Spring Cloud OpenFeign的Starter的依赖 -->
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-openfeign</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>io.github.openfeign</groupId>
  13. <artifactId>feign-okhttp</artifactId>
  14. </dependency>
  15. </dependencies>

application.yml

  1. server:
  2. port: 8011
  3. spring:
  4. application:
  5. name: openfeign-okhttp
  6. feign:
  7. okhttp:
  8. enabled: true
  9. # 日志
  10. logging:
  11. level:
  12. com.msr.better.feign.service.HelloFeignService: debug

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

  1. @Configuration(
  2. proxyBeanMethods = false
  3. )
  4. @ConditionalOnMissingBean({OkHttpClient.class})
  5. public class OkHttpFeignConfiguration {
  6. private OkHttpClient okHttpClient;
  7. public OkHttpFeignConfiguration() {
  8. }
  9. @Bean
  10. @ConditionalOnMissingBean({ConnectionPool.class})
  11. public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
  12. Integer maxTotalConnections = httpClientProperties.getMaxConnections();
  13. Long timeToLive = httpClientProperties.getTimeToLive();
  14. TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
  15. return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
  16. }
  17. @Bean
  18. public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
  19. Boolean followRedirects = httpClientProperties.isFollowRedirects();
  20. Integer connectTimeout = httpClientProperties.getConnectionTimeout();
  21. this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build();
  22. return this.okHttpClient;
  23. }
  24. @PreDestroy
  25. public void destroy() {
  26. if (this.okHttpClient != null) {
  27. this.okHttpClient.dispatcher().executorService().shutdown();
  28. this.okHttpClient.connectionPool().evictAll();
  29. }
  30. }
  31. }

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

  1. @Configuration
  2. @ConditionalOnClass(Feign.class)
  3. @AutoConfigureBefore(FeignAutoConfiguration.class)
  4. public class OkHttpConfig {
  5. @Bean
  6. public okhttp3.OkHttpClient okHttpClient() {
  7. return new okhttp3.OkHttpClient.Builder()
  8. //设置连接超时
  9. .connectTimeout(60, TimeUnit.SECONDS)
  10. //设置读超时
  11. .readTimeout(60, TimeUnit.SECONDS)
  12. //设置写超时
  13. .writeTimeout(60, TimeUnit.SECONDS)
  14. //是否自动重连
  15. .retryOnConnectionFailure(true)
  16. .connectionPool(new ConnectionPool())
  17. //构建OkHttpClient对象
  18. .build();
  19. }
  20. }

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

Post和Get的多参数传递

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

创建cloud-openfeign-eureka-server模块

Eureka Server注册中心

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-actuator</artifactId>
  9. </dependency>
  10. <!-- springboot web -->
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-web</artifactId>
  14. <exclusions>
  15. <exclusion>
  16. <artifactId>spring-boot-starter-tomcat</artifactId>
  17. <groupId>org.springframework.boot</groupId>
  18. </exclusion>
  19. </exclusions>
  20. </dependency>
  21. <!--不用Tomcat,使用undertow -->
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-undertow</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>io.undertow</groupId>
  28. <artifactId>undertow-servlet</artifactId>
  29. </dependency>
  30. </dependencies>

配置文件application.yml

  1. server:
  2. port: 8761
  3. eureka:
  4. instance:
  5. hostname: localhost
  6. server :
  7. enable-self-preservation: false
  8. client:
  9. registerWithEureka: false
  10. fetchRegistry: false
  11. serviceUrl:
  12. defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

启动类

  1. @SpringBootApplication
  2. @EnableEurekaServer
  3. public class EurekaApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(EurekaApplication.class, args);
  6. }
  7. }

创建cloud-openfeign-provider模块

服务提提供者

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.cloud</groupId>
  12. <artifactId>spring-cloud-starter-openfeign</artifactId>
  13. </dependency>
  14. </dependencies>

配置文件application.yml

  1. server:
  2. port: 8012
  3. spring:
  4. application:
  5. name: openfeign-provider
  6. eureka:
  7. client:
  8. serviceUrl:
  9. defaultZone: http://localhost:8761/eureka/
  10. #eureka.instance.prefer-ip-address 表示将自己的IP注册到Eureka Server上,
  11. #如果不配置,会将当前服务提供者所在的主机名注册到Eureka Server上。
  12. instance:
  13. prefer-ip-address: true

实体类和控制器

  1. public class Order {
  2. private Long id;
  3. private String name;
  4. private int age;
  5. public Long getId() {
  6. return id;
  7. }
  8. public void setId(Long id) {
  9. this.id = id;
  10. }
  11. public String getName() {
  12. return name;
  13. }
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17. public int getAge() {
  18. return age;
  19. }
  20. public void setAge(int age) {
  21. this.age = age;
  22. }
  23. }
  1. @RestController
  2. @RequestMapping("/order")
  3. public class OrderController {
  4. @GetMapping(value = "/add")
  5. public String addUser(Order order, HttpServletRequest request) {
  6. String token = request.getHeader("oauthToken");
  7. return "hello," + order.getName();
  8. }
  9. @PostMapping(value = "/update")
  10. public String updateUser(@RequestBody Order order) {
  11. return "hello," + order.getName();
  12. }
  13. }

启动类

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. public class ProviderApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ProviderApplication.class, args);
  6. }
  7. }

创建cloud-openfeign-consumer模块

消费者服务

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.cloud</groupId>
  12. <artifactId>spring-cloud-starter-openfeign</artifactId>
  13. </dependency>
  14. <!-- 使用Apache HttpClient替换Feign原生httpclient -->
  15. <dependency>
  16. <groupId>org.apache.httpcomponents</groupId>
  17. <artifactId>httpclient</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>io.github.openfeign</groupId>
  21. <artifactId>feign-httpclient</artifactId>
  22. </dependency>
  23. </dependencies>

配置文件application.yml

  1. server:
  2. port: 8011
  3. spring:
  4. application:
  5. name: openfeign-consumer
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://localhost:8761/eureka
  10. feign:
  11. httpclient:
  12. enabled: true

实体类

  1. package com.msr.better.feign.model;
  2. public class Order {
  3. private Long id;
  4. private String name;
  5. private int nums;
  6. // 此处省略了getter和setter
  7. }

FeignClient接口

  1. @FeignClient("openfeign-provider")
  2. public interface OrderApiService {
  3. @GetMapping(value = "/order/add")
  4. String addUser(@SpringQueryMap Order order);
  5. @PostMapping(value = "/order/update")
  6. String updateUser(@RequestBody Order order);
  7. }

此处的Client接口中对于GET请求传递实体类使用了注解@SpringQueryMap。OpenFeign@QueryMap批注支持将POJO用作GET参数映射。但是默认的OpenFeign QueryMap注释与Spring不兼容,因为它缺少value属性。

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

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

配置类

  1. @Configuration
  2. public class CoreAutoConfiguration {
  3. @Autowired
  4. private HttpClient httpClient;
  5. @Bean
  6. public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {
  7. HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
  8. factory.setHttpClient(httpClient);
  9. factory.setReadTimeout(3000);
  10. factory.setConnectTimeout(3000);
  11. factory.setConnectionRequestTimeout(3000);
  12. return factory;
  13. }
  14. /**
  15. * {@link RestTemplate }的setRequestFactory方法支持HttpClient和Okhttp等Client
  16. * 默认是使用{@link SimpleClientHttpRequestFactory } Http的请求是使用原生的URLConnection
  17. *
  18. * @return RestTemplate的bean
  19. */
  20. @LoadBalanced
  21. @Bean
  22. public RestTemplate restTemplate() {
  23. RestTemplate restTemplate = new RestTemplate();
  24. restTemplate.setRequestFactory(httpComponentsClientHttpRequestFactory());
  25. return restTemplate;
  26. }
  27. }

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

控制器

  1. @RestController
  2. @RequestMapping("api")
  3. public class OrderController {
  4. @Autowired
  5. private OrderApiService orderApiService;
  6. /**
  7. * @param order
  8. * @return
  9. */
  10. @PostMapping("/get/pojo")
  11. public String getPojo(@RequestBody Order order) {
  12. return orderApiService.addUser(order);
  13. }
  14. @PostMapping("/post/pojo")
  15. String postPojo(@RequestBody Order order){
  16. return orderApiService.updateUser(order);
  17. }
  18. }

最后就可以测试http://localhost:8011/get/pojo和http://localhost:8011/post/pojo了。

文件上传

继续使用上一节创建的Eureka Server。然后创建一下两个模块用作文件上传。

想要实现文件上传功能,需要编写Encoder去实现文件上传。现在OpenFeign提供了子项目feign-form(https://github.com/OpenFeign/feign-form)

创建cloud-openfeign-fileupload-server

文件上传接口的提供者

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  9. </dependency>
  10. </dependencies>

配置文件application.yml

  1. server:
  2. port: 8012
  3. spring:
  4. application:
  5. name: openfeign-file-server
  6. eureka:
  7. server:
  8. enableSelfPreservation: false
  9. client:
  10. serviceUrl:
  11. defaultZone: http://localhost:8761/eureka/
  12. instance:
  13. prefer-ip-address: true

启动类

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. public class UploadServerApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(UploadServerApplication.class, args);
  6. }
  7. }

上传接口

  1. @RestController
  2. public class FileController {
  3. @PostMapping(value = "/uploadFile/server", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  4. public String fileUploadServer(MultipartFile file) {
  5. // save file and return file address
  6. return "http://localhost/" + file.getOriginalFilename();
  7. }
  8. }

创建cloud-openfeign-fileupload-client

文件上传接口的调用者

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  9. </dependency>
  10. <!-- Spring Cloud OpenFeign的Starter的依赖 -->
  11. <dependency>
  12. <groupId>org.springframework.cloud</groupId>
  13. <artifactId>spring-cloud-starter-openfeign</artifactId>
  14. </dependency>
  15. <!-- Feign文件上传依赖-->
  16. <dependency>
  17. <groupId>io.github.openfeign.form</groupId>
  18. <artifactId>feign-form</artifactId>
  19. <version>3.8.0</version>
  20. </dependency>
  21. <dependency>
  22. <groupId>io.github.openfeign.form</groupId>
  23. <artifactId>feign-form-spring</artifactId>
  24. </dependency>
  25. </dependencies>

配置文件application.yml

  1. server:
  2. port: 8011
  3. spring:
  4. application:
  5. name: openfeign-upload-client
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://localhost:8761/eureka

启动类:

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. @EnableFeignClients
  4. public class UploadClientApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(UploadClientApplication.class, args);
  7. }
  8. }

配置类

  1. @Configuration
  2. public class FeignMultipartSupportConfig {
  3. @Bean
  4. @Primary
  5. @Scope("prototype")
  6. public Encoder multipartFormEncoder() {
  7. return new SpringFormEncoder();
  8. }
  9. }

控制器

  1. @RestController
  2. @RequestMapping("file")
  3. public class FeignUploadController {
  4. @Autowired
  5. private FileUploadApiService fileUploadApiService;
  6. @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  7. public String imageUpload(MultipartFile file) throws Exception {
  8. return fileUploadApiService.fileUpload(file);
  9. }
  10. }

FeignClient

  1. @FeignClient(value = "openfeign-file-server", configuration = FeignMultipartSupportConfig.class)
  2. public interface FileUploadApiService {
  3. /***
  4. * 1.produces,consumes必填
  5. * 2.注意区分@RequestPart和RequestParam,不要将
  6. * @RequestPart(value = "file") 写成@RequestParam(value = "file")
  7. * @param file
  8. * @return
  9. */
  10. @PostMapping(value = "/uploadFile/server",
  11. produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
  12. consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  13. String fileUpload(@RequestPart(value = "file") MultipartFile file);
  14. }

测试

  1. 先启动cloud-openfeign-eureka-server
  2. 后启动cloud-openfeign-fileupload-server和cloud-openfeign-fileupload-client
  3. 使用PostMan进行测试。最后成功返回文件的名字,文件成功的上传到server上了。如下图:

解决首次请求失败问题

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

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

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

    1. hystrix:
    2. command:
    3. default:
    4. execution:
    5. isolation:
    6. thread:
    7. timeoutInMillseconds: 5000 # 5秒
  2. 禁用Hystrix的超时时间

    1. hystrix:
    2. command:
    3. default:
    4. execution:
    5. timout:
    6. enable: false
  3. 使用Feign的时候关闭Hystrix,这是不推荐的

    1. feign:
    2. hystrix:
    3. enable: false

返回图片流的处理方式

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

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

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

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

  1. <dependency>
  2. <groupId>cn.hutool</groupId>
  3. <artifactId>hutool-all</artifactId>
  4. <version>5.6.3</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.google.zxing</groupId>
  8. <artifactId>core</artifactId>
  9. <version>3.3.3</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>io.github.openfeign</groupId>
  13. <artifactId>feign-core</artifactId>
  14. </dependency>

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

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

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

添加新依赖

  1. <dependency>
  2. <groupId>commons-io</groupId>
  3. <artifactId>commons-io</artifactId>
  4. <version>2.6</version>
  5. </dependency>

feignclient添加新接口

  1. @GetMapping("/qrcode")
  2. Response getQrCode();

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

这里使用HttpServletResponse,添加方法:

  1. @GetMapping("/qrcode")
  2. public void getQrCode(HttpServletResponse response) {
  3. Response res = fileUploadApiService.getQrCode();
  4. try {
  5. InputStream inputStream = res.body().asInputStream();
  6. response.setContentType(MediaType.IMAGE_PNG_VALUE);
  7. IOUtils.copy(inputStream,response.getOutputStream());
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }

浏览器访问:http://localhost:8011/file/qrcode,结果

调用传递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

修改一下方法,便于展示结果

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

修改cloud-openfeign-consumer

添加拦截器实现feign.RequestInterceptor

  1. @Component
  2. public class FeignTokenInterceptor implements RequestInterceptor {
  3. @Override
  4. public void apply(RequestTemplate requestTemplate) {
  5. if (null == getHttpServletRequest()) {
  6. //此处可以记录一些日志
  7. return;
  8. }
  9. //将获取Token对应的值往下面传
  10. requestTemplate.header("token", getHeaders(getHttpServletRequest()).get("token"));
  11. }
  12. private HttpServletRequest getHttpServletRequest() {
  13. try {
  14. return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
  15. } catch (Exception e) {
  16. return null;
  17. }
  18. }
  19. /**
  20. * Feign拦截器拦截请求获取Token对应的值
  21. *
  22. * @param request
  23. * @return
  24. */
  25. private Map<String, String> getHeaders(HttpServletRequest request) {
  26. Map<String, String> map = new LinkedHashMap<>();
  27. Enumeration<String> enumeration = request.getHeaderNames();
  28. while (enumeration.hasMoreElements()) {
  29. String key = enumeration.nextElement();
  30. String value = request.getHeader(key);
  31. map.put(key, value);
  32. }
  33. return map;
  34. }
  35. }

最后启动服务就可以开始测试了,测试结果:

总结

本文介绍了一些Feign的用法,后续如果有关于Feign新的东西将会新开文章述说。