Feign的目标
feign是声明式的web service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。Spring Cloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。
引入Feign
项目中使用了gradle作为依赖管理,maven类似。
dependencies {
//feign
implementation('org.springframework.cloud:spring-cloud-starter-openfeign:2.0.2.RELEASE')
//web
implementation('org.springframework.boot:spring-boot-starter-web')
//eureka client
implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:2.1.0.M1')
//test
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
因为feign底层是使用了ribbon作为负载均衡的客户端,而ribbon的负载均衡也是依赖于eureka 获得各个服务的地址,所以要引入eureka-client。
SpringbootApplication启动类加上@FeignClient注解,以及@EnableDiscoveryClient。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
yaml配置:
server:
port: 8082
#配置eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
status-page-url-path: /info
health-check-url-path: /health
#服务名称
spring:
application:
name: product
profiles:
active: ${boot.profile:dev}
#feign的配置,连接超时及读取超时配置
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
Feign的使用
@FeignClient(value = "CART")
public interface CartFeignClient {
@PostMapping("/cart/{productId}")
Long addCart(@PathVariable("productId")Long productId);
}
上面是最简单的feign client的使用,声明完为feign client后,其他spring管理的类,如service就可以直接注入使用了,例如:
//这里直接注入feign client
@Autowired
private CartFeignClient cartFeignClient;
@PostMapping("/toCart/{productId}")
public ResponseEntity addCart(@PathVariable("productId") Long productId){
Long result = cartFeignClient.addCart(productId);
return ResponseEntity.ok(result);
}
可以看到,使用feign之后,我们调用eureka 注册的其他服务,在代码中就像各个service之间相互调用那么简单。
FeignClient注解的一些属性
属性名 | 默认值 | 作用 | 备注 |
---|---|---|---|
value | 空字符串 | 调用服务名称,和name属性相同 | |
serviceId | 空字符串 | 服务id,作用和name属性相同 | 已过期 |
name | 空字符串 | 调用服务名称,和value属性相同 | |
url | 空字符串 | 全路径地址或hostname,http或https可选 | |
decode404 | false | 配置响应状态码为404时是否应该抛出FeignExceptions | |
configuration | {} | 自定义当前feign client的一些配置 | 参考FeignClientsConfiguration |
fallback | void.class | 熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。 | 底层依赖hystrix,启动类要加上@EnableHystrix |
path | 空字符串 | 自动给所有方法的requestMapping前加上前缀,类似与controller类上的requestMapping | |
primary | true |
此外,还有qualifier及fallbackFactory,这里就不再赘述。
Feign自定义处理返回的异常
这里贴上GitHub上openFeign的wiki给出的自定义errorDecoder例子。
public class StashErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
//这里是给出的自定义异常
return new StashClientException(
response.status(),
response.reason()
);
}
if (response.status() >= 500 && response.status() <= 599) {
//这里是给出的自定义异常
return new StashServerException(
response.status(),
response.reason()
);
}
//这里是其他状态码处理方法
return errorStatus(methodKey, response);
}
}
自定义好异常处理类后,要在@Configuration修饰的配置类中声明此类。
Feign使用OKhttp发送request
Feign底层默认是使用jdk中的HttpURLConnection发送HTTP请求,feign也提供了OKhttp来发送请求,具体配置如下:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
okhttp:
enabled: true
hystrix:
enabled: true
Feign原理简述
详细原理请参考源码解析。
Feign、hystrix与retry的关系请参考https://xli1224.github.io/2017/09/22/configure-feign/
Feign开启GZIP压缩
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率。
application.yml配置信息如下:
feign:
compression:
request: #请求
enabled: true #开启
mime-types: text/xml,application/xml,application/json #开启支持压缩的MIME TYPE
min-request-size: 2048 #配置压缩数据大小的下限
response: #响应
enabled: true #开启响应GZIP压缩
注意:
由于开启GZIP压缩之后,Feign之间的调用数据通过二进制协议进行传输,返回值需要修改为ResponseEntity<byte[]>才可以正常显示,否则会导致服务之间的调用乱码。
示例如下:
@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);
作用在所有Feign Client上的配置方式
方式一:通过java bean 的方式指定。
@EnableFeignClients注解上有个defaultConfiguration属性,可以指定默认Feign Client的一些配置。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
DefaultFeignConfiguration内容:
@Configuration
public class DefaultFeignConfiguration {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(1000,3000,3);
}
}
方式二:通过配置文件方式指定。
feign:
client:
config:
default:
connectTimeout: 5000 #连接超时
readTimeout: 5000 #读取超时
loggerLevel: basic #日志等级
Feign Client开启日志
日志配置和上述配置相同,也有两种方式。
方式一:通过java bean的方式指定
@Configuration
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.BASIC;
}
}
方式二:通过配置文件指定
logging:
level:
com.xt.open.jmall.product.remote.feignclients.CartFeignClient: debug
Feign 的GET的多参数传递
目前,feign不支持GET请求直接传递POJO对象的,目前解决方法如下:
介绍一个最佳实践,通过feign的拦截器来实现。
@Component
@Slf4j
public class FeignCustomRequestInteceptor implements RequestInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public void apply(RequestTemplate template) {
if (HttpMethod.GET.toString() == template.method() && template.body() != null) {
//feign 不支持GET方法传输POJO 转换成json,再换成query
try {
Map<String, Collection<String>> map = objectMapper.readValue(template.bodyTemplate(), new TypeReference<Map<String, Collection<String>>>() {
});
template.body(null);
template.queries(map);
} catch (IOException e) {
log.error("cause exception", e);
}
}
}
启动时Feign的处理
启动类上使用了@EnableFeignClients注解,我们来看下这个注解在哪里使用了,使用idea只要在EnableFeignClients类上按住command同时点击类名就可以查看到这个类在哪里使用了,发现除了启动类,只在FeignClientsRegistrar类中引用了EnableFeignClients。
debug可以发现,当应用启动时会首先调用FeignClientsRegistrar的registerBeanDefinitions()方法。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册默认配置信息
registerDefaultConfiguration(metadata, registry);
//注册每个声明为Feign Client的类
registerFeignClients(metadata, registry);
}
主要看下registerFeignClients()方法。
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//获取扫描classpath下component组件的扫描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
//获取启动类上配置的@EnableFeignClients注解的属性
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
//从刚才获取的@EnableFeignClients注解的属性中获取clients属性配置的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
//如果clients没配置
//扫描器增加要扫描的过滤器(扫描被@FeignClient注解修饰的类)
scanner.addIncludeFilter(annotationTypeFilter);
//获取配置的扫描包的路径,如果没配置,默认为启动类的包路径
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
//如果启动类配置了clients属性的值,将配置的client所在的包名加到扫描器扫描的包中
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
//遍历包名,扫描@FeignClient注解修饰的类(怎么扫描到?前面加了扫描@FeignClient注解的IncludeFilter)
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
//遍历扫描出来的@FeignClient注解修饰的类
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
//校验@FeignClient注解修饰的类是否是interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
//断言,@FeignClient注解修饰的类必须是interface
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//先获取@FeignClient注解的属性值
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
//获得@FeignClient配置的client 的名称(name或value或serviceId)
String name = getClientName(attributes);
//注册feign client的配置信息
registerClientConfiguration(registry, name,
attributes.get("configuration"));
//注册feign client
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
//将feign client交由spring管理,声明为spring的bean
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
//创建FeignClientFactoryBean,包含将feign client的注解属性信息存入FeignClientFactoryBean中
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
//校验feign client的配置,配置的fallback及fallbackFatory必须是实现类
validate(attributes);
//将@FeignClient注解配置的属性放入FeignClientFactoryBean的BeanDefinitionBuilder中
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
//注册bean到spring容器中
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
在spring容器启动时会调用FeignClientFactoryBean的getObject()方法(只有在其他bean注入feign client时才会调用),看下FeignClientFactoryBean的getObject()方法做了哪些处理。
public Object getObject() throws Exception {
//直接调用了getTarget()方法
return getTarget();
}
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context information
*/
<T> T getTarget() {
//这个FeignContext在FeignAutoConfiguration配置中已经声明了,所以可以直接用applicationContext获取bean
FeignContext context = applicationContext.getBean(FeignContext.class);
//配置feign 的decoder、encoder、retryer、contract、RequestInterceptor等
//这些有默认配置,在FeignAutoConfiguration及FeignClientsConfiguration中有默认配置
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
//如果@FeignClient注解上指定了url,其实除非本地调试,一般不建议指定URL
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
//处理URL,没配置URL时,这里的URL形式为http://name+/path
url += cleanPath();
//使用负载均衡处理feign 请求
return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
//配置了FeignClient的具体URL
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}
从@FeignClient注解上是否指定URL,feign的处理分成了两部分,如果未指定URL,则使用负载均衡去发送请求,指定URL,只会向指定的URL发送请求。
一般是不指定URL的,接下来先看下,不指定具体URL时,feign的处理。
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
//默认client为LoadBalancerFeignClient,为啥?参见DefaultFeignLoadBalancedConfiguration
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
//这个Targeter默认为DefaultTargeter,参见FeignAutoConfiguration
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
Targeter默认为DefaultTargeter,client为LoadBalancerFeignClient。再看下DefaultTargeter.target()方法
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
Feign.target()方法。
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
ReflectiveFeign.newInstance()方法。这里为什么是ReflectiveFeign?参考Feign.build()方法
public <T> T newInstance(Target<T> target) {
//这个apply方法就是ReflectiveFeign中的apply方法,返回了每个方法的调用包装类SynchronousMethodHandler
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
//这个target.type()返回的就是声明@FeignClient注解所在的class
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
//返回了ReflectiveFeign.FeignInvocationHandler对象,这个对象的invoke方法其实就是调用了SynchronousMethodHandler.invoke方法
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
public Map<String, MethodHandler> apply(Target key) {
//获取类上的方法的元数据,如返回值类型,参数类型,注解数据等等
List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
} else if (md.bodyIndex() != null) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
} else {
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
}
//这个factory是SynchronousMethodHandler.Factory,create方法返回了一个SynchronousMethodHandler对象
result.put(md.configKey(),
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
}
return result;
}
简单总结下启动时Feign所做的处理:
这样做的好处是:在程序中使用Feign Client时就可以像其他spring 管理的bean一样直接注入即可。
例如:
@Autowired
private CartFeignClient cartFeignClient;
@PostMapping("/toCart/{productId}")
public ResponseEntity addCart(@PathVariable("productId") Long productId) throws InterruptedException {
cartFeignClient.addCart(productId);
return ResponseEntity.ok(productId);
}
调用Feign Client时的feign的处理
刚分析了应用启动及bean注入FeignClient时feign的处理,知道注入的其实是Targeter类,Targetr类包装了FeignCLient的proxy,proxy内部绑定了methodHandler为SynchronousMethodHandler。接下来仔细分析下整个实际调用过程的处理。
前面提到feign实际处理方法调用的methodHandler是SynchronousMethodHandler。
实际上,首先调用的是ReflectiveFeign的静态内部类FeignInvocationHandler,这个类实现了JDK的InvocationHandler接口,在调用代理类的方法时会被调用FeignInvocationHandler的invoke方法。
FeignInvocationHandler的invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object
otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
//除了equals、hashCode、toString方法外,其他方法都走dispatch.get(method).invoke(args)方法。
//点击这个方法的实现类,就可以追到 SynchronousMethodHandler的invoke方法了。
return dispatch.get(method).invoke(args);
}
可以看到除了equals、hashCode、toString方法外,其他方法都走dispatch.get(method).invoke(args)方法。
点击这个方法的实现类,就可以追到 SynchronousMethodHandler的invoke方法了。所以这里其实只是简单起到转发的作用。
SynchronousMethodHandler的invoke方法。
public Object invoke(Object[] argv) throws Throwable {
//根据调用参数创建一个RequestTemplate,用来具体处理http调用请求
RequestTemplate template = buildTemplateFromArgs.create(argv);
//克隆出一个一模一样的Retryer,用来处理调用失败后的重试
Retryer retryer = this.retryer.clone();
while (true) {
try {
//发送http request以及处理response等
return executeAndDecode(template);
} catch (RetryableException e) {
//处理重试次数、重试间隔等等
retryer.continueOrPropagate(e);
continue;
}
}
}
先来看下如何创建的RequestTemplate。
ReflectiveFeign的内部静态类BuildTemplateByResolvingArgs的create方法。
public RequestTemplate create(Object[] argv) {
//获取methodMetada的template,这个RequestTemplate是可变的,跟随每次调用参数而变。
RequestTemplate mutable = new RequestTemplate(metadata.template());
if (metadata.urlIndex() != null) {
//处理@PathVariable在URL上插入的参数
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.insert(0, String.valueOf(argv[urlIndex]));
}
//处理调用方法的param参数,追加到URL ?后面的参数
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}
//处理query参数以及body内容
RequestTemplate template = resolve(argv, mutable, varBuilder);
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
//当 RequestTemplate处理完参数后,再处理@QueryMap注入的参数,以便优先于任意值。
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}
if (metadata.headerMapIndex() != null) {
//处理RequestTemplate的header内容
template = addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}
return template;
}
可以看到,第一步是根据调用时的参数等构造了RequestTemplate的param、body、header等内容。
再看executeAndDecode方法。
SynchronousMethodHandler的executeAndDecode方法。
Object executeAndDecode(RequestTemplate template) throws Throwable {
//构造Request,将RequestTemplate中的参数等放入Request中
Request request = targetRequest(template);
Response response;
try {
//这个client默认实现是Client接口中的Defalut,实现是通过HttpURLConnection发送请求
//另一种是LoadBalancerFeignClient,默认也是Client接口中的Defalut,可以通过配置指定为Apache的HTTPClient,也可以指定为OKhttp来发送请求,在每个具体实现中来通过ribbon实现负载均衡,负载到集群中不同的机器,这里不再发散
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
} catch (IOException e) {
throw errorExecuting(request, e);
}
boolean shouldClose = true;
try {
//处理response的返回值
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
//根据状态码处理下response
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
}
}
暂无评论内容