Feign使用分析

feign使用

在實現的效果上來說Feign = RestTemplate+Ribbon+Hystrix

Feign實現RestTemplate+Ribbon效果

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效果

首先是先開啓熔斷器

feign:
  hystrix:
    enabled: true

接着增加超時處理邏輯的相關配置

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 15000

但是即使我在被調用方只線程休眠了十秒,程序依然被熔斷了。查閱資料表明,Hystrix將採用feign和hystrix超時時間中較小的那個進行超時判定
file
增加降級兜底方法

@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使用上的其他特性

Feign請求壓縮和響應壓縮配置

配置屬性如下

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爲支持壓縮的數據類型,當前的這幾種類型未默認值

Feign請求日誌配置

首先在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將會打印請求的所有信息如下
file

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方法
file
最終在該方法中實現了遠程調用和負載均衡

歡迎搜索關注本人與朋友共同開發的微信面經小程序【大廠面試助手】和公衆號【微瞰技術】,以及總結的分類面試題https://github.com/zhendiao/JavaInterview

file
file

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章