更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集
1、概述
1.1、Feign是什么
feign是一个声明式的web service客户端,它使得编写web service客户端更加容易。
创建一个接口并且打上注解就可以使用Feign了,它具有可插拔的注解支持,包括Feign注解和JAX-RS注解
feign同样支持可插拔的编码器和解码器
spring cloud为spring mvc的注解添加了支持,可以像spring web一样使用HttpMessageConverters
Feign是一个声明式的、模板化的HTTP客户端,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样
feign默认集成了ribbon和hystrix
2、入门案例
2.1、pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2、application.yml
server:
port: 8030
servlet:
context-path: /consumer-b
spring:
application:
name: consumer-b
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8000/eureka-server/eureka/
register-with-eureka: true
fetch-registry: true
2.3、编写FeignClient
远程服务名为service-a,远程服务的路径为/service-a/common/port
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "service-a",path = "/service-a")
public interface CommonClient {
@RequestMapping(method = RequestMethod.GET, value = "/common/port")
String port();
}
2.4、编写controller
import com.bobo.group.consumerb.service.CommonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CommonController {
@Autowired
private CommonClient commonClient;
@GetMapping("/common/port")
public String port(){
return commonClient.port();
}
}
2.5、启动类打上@EnableFeignClients
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class ConsumerBApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerBApplication.class, args);
}
}
3、关于@EnableFeignClients注解
defaultConfiguration属性:配置所有feignclient的行为
clients属性:手动配置FeignClient
@EnableFeignClients是一个很重要的注解,关于它的原理,请戳这个链接:Spring Cloud教程 第八弹 Feign源码解读
4、配置FeignClient
4.1、默认配置
下面是官方文档给出的默认配置
BeanType | BeanName | ClassName | 说明 |
Decoder | feignDecoder | ResponseEntityDecoder | 封装了SpringDecoder |
Encoder | feignEncoder | SpringEncoder | |
Logger | feignLogger | Slf4jLogger | |
Contract | feignContract | SpringMvcContract | |
Feign.Builder | feignBuilder | HystrixFeign.Builder | |
Client | feignClient | LoadBalancerFeignClient或FeignBlockingLoadBalancerClient或默认的feignClient | 如果ribbon在classpatn上并且enabled,则ClassName为LoadBalancerFeignClient; 如果Spring Cloud LoadBalancer在classpath上,则ClassName为FeignBlockingLoadBalancerClient; 如果两者都没有,则使用默认的feignClient。 |
4.2、全局配置
4.2.1、方式一
@Configuration
public class DefaultConfig {
}
使该类被@ComponentScan扫到,则该类配置的Bean将作用于全局
4.2.2、方式二
@EnableFeignClients(defaultConfiguration = {DefaultConfig.class})
采用@EnableFeignClients的defaultConfiguration属性,也将作用于全局,但此时DefaultConfig类不用打@Configuration注解,也不用被@ComponentScan扫到
4.2.3、方式三
使用配置文件,以feign.client.config.default开头
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
4.3、针对单个FeignClient进行配置
4.3.1、方式一
利用@FeignClient注解的configuration属性,且DefaultConfig类不能被扫到,否则将全局生效
@FeignClient(name = "service-a",path = "/service-a",configuration = {DefaultConfig.class})
4.3.2、方式二
使用配置文件,以feign.client.config.name开头
feign:
client:
config:
service-a:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
4.4、JavaConfig和yml配置文件同时存在
如果配置类和配置属性同时存在,则配置属性优先级更高。可以将default-to-properties设为false让配置类优先级更高
feign:
client:
default-to-properties: false
5、FeignClient
5.1、feign工作原理
- @EnableFeignClients注解会扫描所有的FeignClient,并注入到IOC容器中
- 当Feign接口中的方法被调用时,通过JDK代理,会生成具体的RequestTemplate(每个方法对应一个RequestTemplate对象,该对象封装了HTTP请求的所有信息)
- 由RequestTemplate生成request,再讲request交由client处理,client可以是java原生的URLCOnnection、HttpClient、OKHttp等
- 最后client封装到LoadBalancerClient类,这个类结合负载均衡发起服务间调用
5.2、@FeignClient注解的属性
- value=name=serviceId(不推荐) = 服务名:value和name互相使用了别名(@AliasFor)
- url:手动指定服务地址,不借助eureka server,一般用于调试
- decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
- configuration::Feign配置类
- fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口,在使用fallback属性时,需要使用@Component注解,保证fallback类被Spring容器扫描到
- fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
- path: 定义当前FeignClient的统一前缀
- context:在使用FeignClient时,Spring会按name创建不同的ApplicationContext,如果当多个FeignClient使用同一name,则可以使用ContextId来隔离FeignClient的配置信息
例如:
@FeignClient(name = "service-a",path = "/service-a",contextId = "service-a-common")
public interface CommonClient {
@RequestMapping(method = RequestMethod.GET, value = "/common/port")
String port();
}
@FeignClient(name = "service-a",path = "/service-a",contextId = "service-a-user")
public interface UserClient {
@RequestMapping(method = RequestMethod.GET, value = "/user/list")
List<User> list();
}
6、小知识
6.1、禁用ribbon
Spring Cloud Netflix Ribbon现在处于维护模式,建议改用Spring Cloud LoadBalancer
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
6.2、feign请求超时的解决办法
由于feign默认集成了hystrix,而hystrix默认1秒,由于spring的懒加载机制,往往第一次请求会比较慢,因为要初始化一些类
- 禁用hystrix(不推荐)
- 禁用hystrix的超时时间
- 增大hystrix超时时间
7、Feign调用远程文件下载接口
问题背景是,A服务有一个文件下载接口download,B服务通过feign远程调用该接口。
7.1、服务提供方controller
服务提供方的代码与单体应用一样,通过HttpServletResponse来暴露一个文件下载接口。
download方法上有与业务逻辑相关的参数,不用在意。
@GetMapping(value = "/download",produces = {"application/octet-stream"})
@ApiOperation(value = "文件下载")
public void download(@RequestParam("key") String key,@RequestParam(value = "namespace",required = false) String namespace,HttpServletResponse response) {
try {
response.setHeader("Content-Disposition", "attachment;filename="+new String((key+".zip").getBytes("UTF-8"),"ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
logger.error("不支持编码异常",e);
}
File zipFile = new File("F:/test/aa.zip");
try(BufferedInputStream bin = new BufferedInputStream(new FileInputStream(zipFile));
OutputStream out = response.getOutputStream()){
int len = 0;
byte[] b = new byte[1024];
while ((len = bin.read(b)) != -1) {
out.write(b, 0, len);
}
out.flush();
}catch (IOException e){
logger.error("读取文件失败!",e);
}
}
7.2、服务消费方feign接口
feign接口方法上有与业务逻辑相关的参数,不用在意。
download方法的返回值是重点,注意是feign包下面的Response类。
import feign.Response;
@FeignClient(contextId = "remoteModelParamService", value = ServiceNameConstants.ALGORITHM_MODEL_SERVICE,
path = "/modelparam")
public interface RemoteModelParamService {
@GetMapping(value = "/download",consumes = {"application/octet-stream"})
public Response download(@RequestHeader(CacheConstants.HEADER) String token, @RequestParam("key") String key,
@RequestParam(value = "namespace",required = true) String namespace);
}
7.3、服务消费方controller
消费方的controller如何写是重点。
download方法上有与业务逻辑相关的参数,不用在意。
@GetMapping(value = "/download",produces = {"application/octet-stream"})
public void download(String paramKey, String modelKey, String username, HttpServletResponse response){
// 获取服务提供方的response
Response feignResponse = remoteModelParamService.download(token, paramKey, username + "/" + modelKey);
// 获取服务提供方的response body
Response.Body body = feignResponse.body();
try (BufferedInputStream in = new BufferedInputStream((body.asInputStream()));
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream())) {
response.setContentType("multipart.form-data");
response.setHeader("Content-Disposition", feignResponse.headers().get("Content-Disposition").toString().replace("[","").replace("]",""));
int len = 0;
byte[] b = new byte[1024];
while ((len = in.read(b)) != -1) {
out.write(b, 0, len);
}
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
上面代码的含义其实就是将服务提供方的response复制到服务消费方的response中。
如果没有问题的话,调用服务消费方的文件下载接口与调用服务提供方的文件下载接口效果是一样的都能正确下载文件