Feign 是一个声明式的 Web Service 客户端, 它使得编写 Web Serivce 客户端变得更加简单. 我们只需要使用 Feign 来创建一个接口并用注解来配置它既可完成.
- @FeignClient(value = "qrcodepay-dike-service")
- public interface TestRoute {
- @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
- HdResult get();
- }
我们只需要在相应的接口上添加 @FeignClient 注解即可将他声明为一个 web 客户端. 这其中的原理我们后续分析. 我们首先先关注下 feign 暴露的几个配置.
value: 目标服务名, 一般都是 application.name
fallback : 服务降级策略
- @FeignClient(value = "qrcodepay-dike-service",fallback = TestRoute.TestRouteFaback.class)
- public interface TestRoute {
- @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
- HdResult get();
- }
- @Component
- class TestRouteFaback implements TestRoute{
- @Override
- public HdResult get() {
- return HdResult.makeFail("服务降级");
- }
- }
fallbackFactory :fallback 的升级版, 可以获取更加详细的异常信息
- @FeignClient(value = "qrcodepay-dike-service",fallbackFactory = TestRoute.TestRouteFallbackFactory.class)
- public interface TestRoute {
- @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
- HdResult get();
- @Component
- class TestRouteFallbackFactory implements FallbackFactory<TestRoute>{
- private static final Logger logger = LoggerFactory.getLogger(TestRouteFallbackFactory.class);
- @Override
- public TestRoute create(Throwable throwable) {
- String msg = throwable == null ? "" : throwable.getMessage();
- if (!StringUtils.isEmpty(msg)) {
- logger.error("异常信息打印:{}",msg);
- }
- return new TestRoute() {
- @Override
- public HdResult get() {
- return HdResult.makeFail(msg);
- }
- };
- }
- }
- }
configuration: 重写 feign 的配置
具体哪些内容可以配置我们可以看 FeignClientsConfiguration 和 feign.Feign.Builder.
下面用两种方式重写 feign 的配置
覆盖原有的配置 bean 达到重写目的
- @Configuration
- public class FeignBreakerConfiguration {
- @Bean
- public ErrorDecoder errorDecoder() {
- return new UserErrorDecoder();
- }
- /**
- * 自定义错误解码器 只有返回 http status 非 200 才会进入
- */
- public class UserErrorDecoder implements ErrorDecoder {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Override
- public Exception decode(String methodKey, Response response) {
- Exception exception = null;
- try {
- String json = Util.toString(response.body().asReader());
- System.out.println("自定义解码:"+json);
- exception = new RuntimeException(json);
- HdResult result=HdResult.makeFail(json);
- // 业务异常包装成 HystrixBadRequestException, 不进入熔断逻辑
- // if (!result.isSuccess()) {
- // exception = new HystrixBadRequestException(result.getMessage());
- // }
- } catch (IOException ex) {
- logger.error(ex.getMessage(), ex);
- }
- return exception;
- }
- }
- }
自定义客户端达到重写的目的
- @Import(FeignClientsConfiguration.class)
- @RestController
- public class DefaultController {
- private FeignClientService feignClientService;
- public DefaultController(Decoder decoder, Encoder encoder, Client client){
- this.feignClientService = Feign.builder().client(client)
- .encoder(encoder)
- .decoder(decoder)
- .requestInterceptor(new BasicAuthRequestInterceptor("user","password"))
- .target(FeignClientService.class,"http://eureka-client");
- }
- @RequestMapping(name = "/default",method = RequestMethod.GET)
- public String getInfo(){
- return feignClientService.getValue("hello world!");
- }
- }
feignclient 最常用的配置大致如上, 接下来介绍下 feign 实现的原理.
先说结论, feign 是通过动态代理的技术将一个 interface 变为 Web Service 客户端. 那我们应该从哪里入手呢. 在使用 feign 的时候, 我们应该关注两个注解, 一个就是我们上文所说的 feignClient, 但是仅仅只用这个注解 feign 是不会生效的, 必须要在启动类上加上 EnableFeignClients,feign 才会自动扫描 feignClient. 所以我们的入口应该是 EnableFeignClients
EnableFeignClients 导入了 FeignClientsRegistrar, 这个注解真正的逻辑就在 FeignClientsRegistrar 中
这个类实现了三个接口, 我们先关注 ImportBeanDefinitionRegistrar, 这是 spring 动态注册 bean 的接口. 所以 spring 在启动的时候会调用以下方法
- public void registerBeanDefinitions(AnnotationMetadata metadata,
- BeanDefinitionRegistry registry) {
- registerDefaultConfiguration(metadata, registry);
- registerFeignClients(metadata, registry);
- }
将配置类纳入 beandefinationMap 管理 , 这一块更为详细的内容可以看 SpringIoc 分析
- private void registerDefaultConfiguration(AnnotationMetadata metadata,
- BeanDefinitionRegistry registry) {
- Map<String, Object> defaultAttrs = metadata
- .getAnnotationAttributes(EnableFeignClients.class.getName(), true);
- if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
- String name;
- if (metadata.hasEnclosingClass()) {
- name = "default." + metadata.getEnclosingClassName();
- }
- else {
- name = "default." + metadata.getClassName();
- }
- registerClientConfiguration(registry, name,
- defaultAttrs.get("defaultConfiguration"));
- }
- }
扫描 FeignClient 注解, 将 interface 纳入 beanDefination
- public void registerFeignClients(AnnotationMetadata metadata,
- BeanDefinitionRegistry registry) {
- ClassPathScanningCandidateComponentProvider scanner = getScanner();
- scanner.setResourceLoader(this.resourceLoader);
- Set<String> basePackages;
- Map<String, Object> attrs = metadata
- .getAnnotationAttributes(EnableFeignClients.class.getName());
- AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
- FeignClient.class);
- final Class<?>[] clients = attrs == null ? null
- : (Class<?>[]) attrs.get("clients");
- if (clients == null || clients.length == 0) {
- scanner.addIncludeFilter(annotationTypeFilter);
- basePackages = getBasePackages(metadata);
- }
- else {
- final Set<String> clientClasses = new HashSet<>();
- basePackages = new HashSet<>();
- for (Class<?> clazz : clients) {
- 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)));
- }
- for (String basePackage : basePackages) {
- Set<BeanDefinition> candidateComponents = scanner
- .findCandidateComponents(basePackage);
- for (BeanDefinition candidateComponent : candidateComponents) {
- if (candidateComponent instanceof AnnotatedBeanDefinition) {
- // verify annotated class is an interface
- AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
- AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
- Assert.isTrue(annotationMetadata.isInterface(),
- "@FeignClient can only be specified on an interface");
- Map<String, Object> attributes = annotationMetadata
- .getAnnotationAttributes(
- FeignClient.class.getCanonicalName());
- String name = getClientName(attributes);
- registerClientConfiguration(registry, name,
- attributes.get("configuration"));
- registerFeignClient(registry, annotationMetadata, attributes);
- }
- }
- }
- }
接下来, 我们需要找到 jdk 代理的地方
我们在构建 feign 的地方发现如下方法
- public Feign build() {
- SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
- new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
- logLevel, decode404);
- ParseHandlersByName handlersByName =
- new ParseHandlersByName(contract, options, encoder, decoder,
- errorDecoder, synchronousMethodHandlerFactory);
- return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
- }
最终我们在 SynchronousMethodHandler 类中发现了真正拦截的代码
- 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) {
- retryer.continueOrPropagate(e);
- if (logLevel != Logger.Level.NONE) {
- logger.logRetry(metadata.configKey(), logLevel);
- }
- continue;
- }
- }
- }
真正执行的逻辑如下, 这里也是 feign 最为关键的地方. 这里我们主要关注下真正请求的那一行. 如果想对 feign 做 debug 或者重写一些配置, 参考这里会是一个很好的入口.
- Object executeAndDecode(RequestTemplate template) throws Throwable {
- Request request = targetRequest(template);
- if (logLevel != Logger.Level.NONE) {
- logger.logRequest(metadata.configKey(), logLevel, request);
- }
- Response response;
- long start = System.nanoTime();
- try {
- response = client.execute(request, options);
- // ensure the request is set. TODO: remove in Feign 10
- response.toBuilder().request(request).build();
- } catch (IOException e) {
- if (logLevel != Logger.Level.NONE) {
- logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
- }
- throw errorExecuting(request, e);
- }
- long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
- boolean shouldClose = true;
- try {
- if (logLevel != Logger.Level.NONE) {
- response =
- logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
- // ensure the request is set. TODO: remove in Feign 10
- response.toBuilder().request(request).build();
- }
- if (Response.class == metadata.returnType()) {
- if (response.body() == null) {
- return response;
- }
- if (response.body().length() == null ||
- response.body().length()> MAX_RESPONSE_BUFFER_SIZE) {
- shouldClose = false;
- return response;
- }
- // Ensure the response body is disconnected
- byte[] bodyData = Util.toByteArray(response.body().asInputStream());
- return response.toBuilder().body(bodyData).build();
- }
- if (response.status()>= 200 && response.status() <300) {
- if (void.class == metadata.returnType()) {
- return null;
- } else {
- return decode(response);
- }
- } else if (decode404 && response.status() == 404) {
- return decoder.decode(response, metadata.returnType());
- } else {
- throw errorDecoder.decode(metadata.configKey(), response);
- }
- } catch (IOException e) {
- if (logLevel != Logger.Level.NONE) {
- logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
- }
- throw errorReading(request, response, e);
- } finally {
- if (shouldClose) {
- ensureClosed(response.body());
- }
- }
- }
这里的 client 是请求客户端, feign 统一封装为 LoadBalancerFeignClient
- @ConditionalOnClass({ ILoadBalancer.class, Feign.class })
- @Configuration
- @AutoConfigureBefore(FeignAutoConfiguration.class)
- public class FeignRibbonClientAutoConfiguration {
- @Bean
- @ConditionalOnMissingBean
- public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
- SpringClientFactory clientFactory) {
- return new LoadBalancerFeignClient(new Client.Default(null, null),
- cachingFactory, clientFactory);
- }
- }
默认的 Client 是 HttpURLConnection, 同时 feign 也支持 httpclient 和 okhhtp
- @Configuration
- @ConditionalOnClass(ApacheHttpClient.class)
- @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
- @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
- protected static class HttpClientFeignConfiguration {
- @Autowired(required = false)
- private HttpClient httpClient;
- @Bean
- @ConditionalOnMissingBean(Client.class)
- public Client feignClient() {
- if (this.httpClient != null) {
- return new ApacheHttpClient(this.httpClient);
- }
- return new ApacheHttpClient();
- }
- }
- @Configuration
- @ConditionalOnClass(OkHttpClient.class)
- @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
- @ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
- protected static class OkHttpFeignConfiguration {
- @Autowired(required = false)
- private okhttp3.OkHttpClient okHttpClient;
- @Bean
- @ConditionalOnMissingBean(Client.class)
- public Client feignClient() {
- if (this.okHttpClient != null) {
- return new OkHttpClient(this.okHttpClient);
- }
- return new OkHttpClient();
- }
- }
只要满足 配置条件, 就可以将 httpclient 或 okhhtp 引入, 这里举例说明怎么使用 httpclient
在 pom 文件加上:
- <dependency>
- <groupId>com.netflix.feign</groupId>
- <artifactId>feign-httpclient</artifactId>
- <version>RELEASE</version>
- </dependency>
在配置文件上加上 feign.httpclient.enabled 为 true(默认为 true, 可不写)
最后, 我们再看看 feign 是怎么使用 ribbon 的, 上文我们说过 feign 统一将 client 封装为 LoadBalancerFeignClient,fein 的请求最终都会到以下代码
- public Response execute(Request request, Request.Options options) throws IOException {
- try {
- URI asUri = URI.create(request.url());
- String clientName = asUri.getHost();
- URI uriWithoutHost = cleanUrl(request.url(), clientName);
- FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
- this.delegate, request, uriWithoutHost);
- IClientConfig requestConfig = getClientConfig(options, clientName);
- return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
- requestConfig).toResponse();
- }
- catch (ClientException e) {
- IOException io = findIOException(e);
- if (io != null) {
- throw io;
- }
- throw new RuntimeException(e);
- }
- }
具体我们可以看下 executeWithLoadBalancer
- public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
- RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
- LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
- .withLoadBalancerContext(this)
- .withRetryHandler(handler)
- .withLoadBalancerURI(request.getUri())
- .build();
- try {
- return command.submit(
- new ServerOperation<T>() {
- @Override
- public Observable<T> call(Server server) {
- URI finalUri = reconstructURIWithServer(server, request.getUri());
- S requestForServer = (S) request.replaceUri(finalUri);
- try {
- return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
- }
- catch (Exception e) {
- return Observable.error(e);
- }
- }
- })
- .toBlocking()
- .single();
- } catch (Exception e) {
- Throwable t = e.getCause();
- if (t instanceof ClientException) {
- throw (ClientException) t;
- } else {
- throw new ClientException(e);
- }
- }
- }
在 submit 方法里, 发现了如下代码
- // Use the load balancer
- Observable<T> o =
- (server == null ? selectServer() : Observable.just(server))
- .concatMap(new Func1<Server, Observable<T>>() {
- }
这里的 selectServer 最终会调用 ILoadBalancer 选择一个 server
- ILoadBalancer lb = getLoadBalancer();
- if (host == null) {
- // Partial URI or no URI Case
- // well we have to just get the right instances from lb - or we fall back
- if (lb != null){
- Server svc = lb.chooseServer(loadBalancerKey);
关于这方面的具体内容, 请参考 SpringCloud Ribbon 的分析
以上, 就是对 feign 的具体分析
来源: https://www.cnblogs.com/xmzJava/p/9612988.html