在实现的效果上来说Feign = RestTemplate+Ribbon+Hystrix
Feign实现RestTemplate+Ribbon效果,只需要以下几步
在springcloud项目调用方的pom文件中加入openFeign的配置
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
在启动类中加入@EnableFeignClients
同时使用接口声明的方式来实现接口调用
@FeignClient(name = "zhao-service-resume") public interface ResumeFeignClient { @GetMapping("/resume/openstate/{userId}") public Integer findDefaultResumeState(@PathVariable Long userId) ; }
这个接口的声明与被调用方的实现完全一样,我们需要在声明时@FeignClient(name = "zhao-service-resume")声明被调用的服务,即可按照默认的方式进行调用
使用单元测试测试即可测试到负载均衡的效果,访问两次,分别访问到8081和8082端口的服务
@RunWith(SpringRunner.class) @SpringBootTest(classes = DeliverApplication8091.class) public class FeignTest { @Autowired ResumeFeignClient feignClient; @Test public void feignTest(){ Integer port = feignClient.findDefaultResumeState(2195321L); System.out.println("测试的结果"+port); } }
那么如何在配置类中配置负载均衡呢?格式如下,同时我们还配置了请求的超时时间,在没有配置hystrix的情况下,会出现超时的情况,
zhao-service-resume: ribbon: #请求连接超时时间 ConnectTimeout: 2000 #请求处理超时时间 ReadTimeout: 5000 #对所有操作都进⾏重试 OkToRetryOnAllOperations: true ####根据如上配置,当访问到故障请求的时候,它会再尝试访问⼀次当前实例(次数由MaxAutoRetries配置), ####如果不⾏,就换⼀个实例进⾏访问,如果还不⾏,再换⼀次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不⾏,返回失败信息。 MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第⼀次调⽤ MaxAutoRetriesNextServer: 0 #切换实例的重试次数 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
超时的报错feign.RetryableException: Read timed out executing GET http://zhao-service-resume/resume/openstate/2195321
即是没有配置重试的这几个参数也是同样的效果
首先是先开启熔断器
feign: hystrix: enabled: true
接着增加超时处理逻辑的相关配置
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000
但是即使我在被调用方只线程休眠了十秒,程序依然被熔断了。查阅资料表明,Hystrix将采用feign和hystrix超时时间中较小的那个进行超时判定
增加降级兜底方法
@Component public class FeignClientFallBack implements ResumeFeignClient { @Override public Integer findDefaultResumeState(Long userId) { return -1; } }
在调用方增加降级配置
@FeignClient(name = "zhao-service-resume",fallback = FeignClientFallBack.class) public interface ResumeFeignClient { @GetMapping("/resume/openstate/{userId}") public Integer findDefaultResumeState(@PathVariable Long userId) ; }
同时可以在@FeignClient中增加path属性,将方法上的公共路径提取到类中
配置属性如下
feign: compression: request: enabled: true min-request-size: 2048 mime-types: text/html,application/xml,application/json # 设置压缩的数据类型 response: enabled: true
以上配置包含的两个属性,min-request-size: 2048表示开启压缩的最小值为2048字节,mime-types为支持压缩的数据类型,当前的这几种类型未默认值
首先在yml中设置具体的类的日志响应级别
logging: level: # Feign⽇志只会对⽇志级别为debug的做出响应 com.lagou.edu.controller.service.ResumeServiceFeignClient: debug
然后就是针对feign的log的配置
import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfig { @Bean Logger.Level feignLevel(){ return Logger.Level.FULL; } }
需要注意的是,此处引入的是feign.Logger,此处表示的含义是feign将会打印请求的所有信息如下
还是依据前文,依照启动类注解和spring.factories中配置的自动配置类来进行分析,首先我们看@EnableFeignClients注解中的FeignClientsRegistrar的具体内容,实现的依然是Spring中的注入beanDefinition的内容
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
registerDefaultConfiguration注入默认配置我们基本可以确定就是加入一些默认配置,而registerFeignClients就是最终实现逻辑的地方。而最终实现逻辑的地方是在该方法下的
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); String contextId = getContextId(attributes); definition.addPropertyValue("contextId", contextId); 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 = contextId + "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 }); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
而这个类都依赖于 BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);几乎可以确定注入的时候就是FeignClientFactoryBean这个工厂Bean在起作用,那么我们在进入里面看一看,工厂Bean最重要的就是getObject返回的类型情况
@Override public Object getObject() throws Exception { 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 context = applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { url = "http://" + this.name; } else { url = this.name; } url += cleanPath(); return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, 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)); }
在上述代码中,基本上就是构造客户端并调用的过程,那么最关键的就是实现了Ribbon功能的负载均衡的loadBalance操作中内容
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); 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.target这段最后都会执行到feign类中的这个方法中
public <T> T target(Target<T> target) { return build().newInstance(target); }
关注到newInstance方法发现最终实现时在ReflectiveFeign类中
@Override public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); 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))); } } 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; }
上述可见,最终生成的类实际上一个代理类完成了最终的调用,而在代理对象就完成了最后的负载均衡等处理,生成代理对象使用的死FeignInvocationHandler的invoke方法
static final class Default implements InvocationHandlerFactory { @Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); } }
最后执行了相关的编解码操作
@Override public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } }
而执行并解码的操作executeAndDecode中最重要的就是client.execute方法,点进去之后发现,居然最终调用的就是LoadBalancerFeignClient.execute方法
最终在该方法中实现了远程调用和负载均衡
欢迎搜索关注本人与朋友共同开发的微信面经小程序【大厂面试助手】和公众号【微瞰技术】,以及总结的分类面试题https://github.com/zhendiao/JavaInterview