文章首发: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去做。
- 可插拔的注解反对,包含Feign注解和JSX-RS注解
- 反对可插拔的HTTP编码器和解码器
- 反对Hystrix和它的Fallback
- 反对Ribbon的负载平衡
- 反对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: 8010spring: application: name: openfeign-hello# 日志logging: level: com.msr.better.feign.service.HelloFeignService: debug
配置类
@Configurationpublic 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
@RestControllerpublic class HelloFeignController { @Autowired private HelloFeignService helloFeignService; @GetMapping(value = "/search/github") public String searchGithubRepoByStr(@RequestParam("searchStr") String searchStr) { return helloFeignService.searchRepo(searchStr); }}
启动类
@SpringBootApplication@EnableFeignClientspublic 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)@Documentedpublic @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: 8011spring: application: name: openfeign-gziplogging: level: com.msr.better.feign.service.HelloFeignService: debugfeign: # 压缩配置 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 TYPEfeign.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: 8011spring: application: name: openfeign-gziplogging: level: com.msr.better.feign.service.HelloFeignService: debugfeign: # 压缩配置 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注解中执行改配置类。
@Configurationpublic 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: 8010spring: application: name: openfeign-httpclientfeign: 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: 8011spring: application: name: openfeign-okhttpfeign: 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: 8761eureka: instance: hostname: localhost server : enable-self-preservation: false client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类
@SpringBootApplication@EnableEurekaServerpublic 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: 8012spring: application: name: openfeign-providereureka: 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@EnableDiscoveryClientpublic 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: 8011spring: application: name: openfeign-consumereureka: client: service-url: defaultZone: http://localhost:8761/eurekafeign: 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
的反对了。
配置类
@Configurationpublic 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: 8012spring: application: name: openfeign-file-servereureka: server: enableSelfPreservation: false client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
启动类
@SpringBootApplication@EnableDiscoveryClientpublic class SCFeignFileServerApplication { public static void main(String[] args) { SpringApplication.run(SCFeignFileServerApplication.class, args); }}
上传接口
@RestControllerpublic 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: 8011spring: application: name: openfeign-upload-clienteureka: client: service-url: defaultZone: http://localhost:8761/eureka
配置类
@Configurationpublic 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而导致申请失败。解决办法:
将Hystrix的超时工夫调大,此办法比拟好
hystrix: command: default: execution: isolation: thread: timeoutInMillseconds: 5000 # 5秒
禁用Hystrix的超时工夫
hystrix: command: default: execution: timout: enable: false
应用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
@Componentpublic 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新的货色将会新开文章述说。