第三章 微服务-服务调用

导读:本篇文章讲解 第三章 微服务-服务调用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

3.1 RestTemplate简介

        RestTemplate是Spring Resources中⼀个访问第三⽅RESTful API接⼝的⽹络请求框架。

        RestTemplate的设计原则和其他的Spring Template(例如JdbcTemplate)类似,都是为了执⾏复杂任务提供了⼀个具有默认⾏为的简单⽅法。

        RestTemplate是⽤来消费REST服务的,所以RestTemplate的主要⽅法都与REST的HTTP协议的⼀些⽅法紧密相连,例如HEAD、GET、POST、PUT、DELETE、OPTIONS等⽅法,这些⽅法在RestTemplate类对应的⽅法为headForHeaders(),getForObject()、postForObject()、put()、delet() 等。

        举例说明,在订单服务通过RestTemplate的getForObject⽅法调⽤⽀付服务,并且将调⽤结果反序列化成Payment对象,代码如下。

@GetMapping("/payment/{id}") 
public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) { 
    String url = "http://localhost:9001/payment/" + id; 
    List<ServiceInstance> serviceInstances = discoveryClient.getInstances("cloud-payment-service"); 
    ServiceInstance serviceInstance = serviceInstances.get(0); 
    url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/payment/" + id; 
    Payment payment = restTemplate.getForObject(url, Payment.class); 
    return ResponseEntity.ok(payment); 
} 

        RestTemplate⽀持常⻅的Http协议请求⽅法,例如post, get, delete等,所以⽤RestTemplate很容易构建RESTfule API。上述案例结果返回json对象,使⽤jackson框架完成。

3.2 LoadBalancer负载均衡

        负载均衡是指将负载分摊到多个执⾏单元上,常⻅的负载均衡有两种⽅式。⼀种独⽴进程单元,通过负载均衡策略,将请求转发到不同的执⾏单元上,例如Nginx。另⼀种是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了⼀份服务提供者的信息列表,有了信息表,通过负载均衡策略将请求分摊给多个服务提供者,从⽽达到负载均衡的⽬的。

        SpringCloud原有的客户端负载均衡⽅案Ribbon已经被废弃,取⽽代之的是SpringCloud LoadBalancer,LoadBalancer是Spring Cloud Commons的⼀个⼦项⽬,他属于上述的第⼆种⽅式,是将负载均衡逻辑封装到客户端中,并且运⾏在客户端的进程⾥。

        在Spring Cloud构件微服务系统中,LoadBalancer作为服务消费者的负载均衡器,有两种使⽤⽅式,⼀种是和RestTemplate相结合,另⼀种是和Feign相结合,Feign已经默认集成了LoadBalancer, 关于Feign下⼀章讲解。

3.2.1 LoadBalancer整合RestTemplate

在⽀付微服务⼯程中进⾏如下更改。

1.配置⽂件

        在application.yml配置⽂件中,使⽤spel指定端⼝,表示存在port参数使⽤port参数,不存在使⽤默认9001端⼝, 启动⽀付服务时,可以通过指定-Dport=9000,指定⽀付服务使⽤不同端⼝启动,具体参考后⾯内容。

server: 
port: ${port:9001} 

2.PaymentController

        在提供⽀付服务时,把端⼝打印出来,⽅便查看测试效果。代码如下。

@RestController 
@RequestMapping("/payment") 
public class PaymentController { 
​
    @Value("${server.port}") 
    private String serverPort; 
​
    @GetMapping("/{id}") 
    public ResponseEntity<Payment> payment(@PathVariable("id") Integer id) { 
    Payment payment = new Payment(id, "⽀付成功,服务端⼝=" + serverPort); 
    return ResponseEntity.ok(payment); 
    } 
​
} 

3.OrderApplication

        在产⽣RestTemplate实例时,使⽤@LoadBalanced注解,开启负载均衡。

@Bean 
@LoadBalanced 
public RestTemplate restTemplate() { 
    return new RestTemplate(); 
} 

4.OrderController

        在OrderController中,使⽤serviceId调⽤⽀付服务,此时LoadBalancer负载均衡⽣效,从多个服务提供者节点轮询选择⼀个使⽤。

@GetMapping("/payment/{id}") 
public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) { 
    String url = "http://cloud-payment-service/payment/" + id; 
    Payment payment = restTemplate.getForObject(url, Payment.class); 
    return ResponseEntity.ok(payment); 
} 

5.启动并测试

        启动两个⽀付微服务⼯程,端⼝分别是9000和9001,因为application.yml配置⽂件中使⽤ ${port:9001}配置端⼝,其中9000节点启动配置如图3-1所示。

第三章 微服务-服务调用

         另⼀个⽀付节点,不指定-Dport,使⽤默认9001端⼝启动,这时准备了2个⽀付微服务节点。 Eureka注册效果如图3-2所示。

        接下来在浏览器多次访问http://localhost:9002/order/payment/123456时,负载均衡器起了作⽤,结果9000,9001轮流出现。

  • ⽀付成功,服务端⼝=9000
  • ⽀付成功,服务端⼝=9001

3.2.2 LoadBlancerClient简介

        负载均衡的核⼼类为LoadBalancerClient,LoadBalancerClient可以获取负载均衡的服务提供者实例信息。在OrderController增加演示代码如下。

@Autowired 
private LoadBalancerClient loadBalancerClient; 
​
@GetMapping("/test-load-balancer") 
public String testLoadBalancer() { 
    ServiceInstance instance = loadBalancerClient.choose("cloud-payment-service");
    return instance.getHost() + ":" + instance.getPort(); 
} 

重启⼯程,浏览器访问http://localhost:9002/order/test-load-balancer,发现浏览器轮流显示如下内容

  • localhost:9000
  • localhost:9001

3.3 LoadBalancer源码解析

3.3.1 类的调用顺序

        当时使⽤有@LoadBalanced注解的RestTemplate时,设计的类的调⽤关系如图所示

第三章 微服务-服务调用

 关键类解析

  • LoadBalancerRequestFactory: ⼀个⼯⼚, 包装⼀个为HttpRequest对象,回调对象 LoadBalancerRequest。

  • LoadBalancerClient: ⽤于根据 serviceId 选取⼀个 ServiceInstance, 执⾏从 LoadBalancerRequestFactory 获得的那个回调。

  • LoadBalancerInterceptor:RestTemplate 的拦截器, 拦截后调⽤ LoadBalancerClient 修改 HttpRequest 对象(主要是 url), 且传⼊调⽤ LoadBalancerRequestFactory ⽣成的回调给 LoadBalancerClient。

  • RestTemplateCustomizer:为 restTemplate 加上⼀个拦截器(也可以⼲点别的, 默认就这⼀个⽤处) 。

  • SmartInitializingSingleton:容器初始化是,调⽤ RestTemplateCustomizer 为容器中所有加了。 @LoadBalanced 的 RestTemplate 加上⼀个拦截器。

  • ReactiveLoadBalancer:定义了 choose ⽅法, 即如何选取⼀个 ServiceInstance, 如轮播, 随机。

3.3.2 源码部分

1.LoadBalancerInterceptor

        RestTemplate 的拦截器, 拦截后调⽤ LoadBalancerClient 修改 HttpRequest 对象(主要是 url), 且传⼊调⽤ LoadBalancerRequestFactory ⽣成的回调给 LoadBalancerClient。

@Override 
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { 
​
    final URI originalUri = request.getURI(); 
    String serviceName = originalUri.getHost(); 
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); 
    return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); 
​
} 

2.BlockingLoadBalancerClient

        ⽤于根据 serviceId 选取⼀个 ServiceInstance, 执⾏从 LoadBalancerRequestFactory 获得的那 个回调,发送HTTP请求,远程调⽤REST ful API。代码如下所示。

@Override 
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { 
​
    String hint = getHint(serviceId); 
    LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request, 
    new DefaultRequestContext(request, hint)); 
​
    Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId); 
​
    supportedLifecycleProcessors.forEach(lifecycle -> 
        lifecycle.onStart(lbRequest)); 
    ServiceInstance serviceInstance = choose(serviceId, lbRequest); 
    if (serviceInstance == null) { 
        supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete( 
        new CompletionContext<>(CompletionContext.Status.DISCARD, 
        lbRequest, new EmptyResponse()))); 
        throw new IllegalStateException("No instances available for " + serviceId); 
    }
    return execute(serviceId, serviceInstance, lbRequest); 
} 

        choose⽅法,调⽤RoundRobinLoadBalancer的choose⽅法,以轮播⽅式获取⼀个serviceInstance。代码如下所示。

@Override 
public <T> ServiceInstance choose(String serviceId, Request<T> request) { 
​
    ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId); 
    if (loadBalancer == null) { 
        return null; 
    }
    Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block(); 
    if (loadBalancerResponse == null) { 
        return null; 
    }
    return loadBalancerResponse.getServer(); 
​
} 

3.RoundRobinLoadBalancer

RoundRobinLoadBalancer的choose⽅法,以轮播⽅式获取⼀个serviceInstance。代码如下所示。

public Mono<Response<ServiceInstance>> choose(Request request) { 
​
    ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider 
    .getIfAvailable(NoopServiceInstanceListSupplier::new); 
    return supplier.get(request).next() 
    .map(serviceInstances -> processInstanceResponse(supplier, 
    serviceInstances)); 
​
} 

3.4 Spirng Cloud OpenFeign

3.4.1 ⼊⻔案例

        Feign是⼀个声明式的HTTP客户端组件,它旨在是编写Http客户端变得更加容易。OpenFeign添加了对于Spring MVC注解的⽀持,同时集成了Spring Cloud LoadBalancer和Spring Cloud CircuitBreaker,在使⽤Feign时,提供负载均衡和熔断降级的功能。

1.添加依赖

在订单⼯程⼯程的pom.xml中添加如下依赖

<dependency> 
<groupId>org.springframework.cloud</groupId> 
<artifactId>spring-cloud-starter-openfeign</artifactId> 
</dependency> 

2.开启Feign功能

使⽤@EnableFeignClients开启Feign功能

@SpringBootApplication 
@EnableDiscoveryClient 
@EnableFeignClients 
public class OrderApplication { 
    public static void main(String[] args) { 
    SpringApplication.run(OrderApplication.class, args); 
    }
​
    @Bean 
    @LoadBalanced 
    public RestTemplate restTemplate() { 
    return new RestTemplate(); 
    } 
​
} 

3.创建Feign客户端

        在注解@FeignClient注解中,“cloud-payment-service”是服务名,使⽤这个名字来从Eureka服务列表中得到相应的服务,来创建LoadBalancer客户端,也可以使⽤url属性,指定服务的URL。

@FeignClient(value = "cloud-payment-service") 
public interface PaymentClient { 
    @GetMapping("/payment/{id}") 
    public Payment payment(@PathVariable("id") Integer id); 
} 

4.OrderController

在OrderController中使⽤FeignClient访问⽀付服务,代码如下。

@Autowired 
private PaymentClient paymentClient; 
​
@GetMapping("/feign/payment/{id}") 
public ResponseEntity<Payment> getPaymentByFeign(@PathVariable("id") Integer id) { 
    Payment payment = paymentClient.payment(id); 
    return ResponseEntity.ok(payment); 
} 

5.启动并测试

        分别启动⽀付服务9000端⼝,9001端⼝,订单服务,访问http://localhost:9002/order/feign/payment/123

        多次执⾏发现9000、9001,顺序显示。因此得知Feign集成了负载均衡LoadBalancer组件

3.4.2 超时配置

        通过分析上述案例的执⾏现象,得到结论OpenFeign集成了负载均衡组件LoadBalancer, OpenFeign提供了2个超时参数。

  • connectTimeout防⽌由于服务器处理时间⻓⽽阻塞调⽤者。

  • readTimeout 从连接建⽴时开始应⽤,在返回响应时间过⻓时触发。

对于所有的FeignClient配置,可以使⽤”default”假名,代码如下。

feign: 
client: 
config: 
default: 
connectTimeout: 5000 #防⽌由于服务器处理时间⻓⽽阻塞调⽤者 
readTimeout: 5000 #从连接建⽴时开始应⽤,在返回响应时间过⻓时触发

如果只对于具体FeignClient配置,可以把default换成具体的FeignClient的名字,代码如下。

feign: 
client: 
config: 
feignName: 
connectTimeout: 5000 #防⽌由于服务器处理时间⻓⽽阻塞调⽤者 
readTimeout: 5000 #从连接建⽴时开始应⽤,在返回响应时间过⻓时触发 

3.4.3 集成熔断器

        Feign可以集成Spring Cloud CircuitBreaker熔断器,集成后,Feign将使⽤断路器包装的所有⽅法。具体⽤法如下。

1.添加依赖

在订单⼯程pom.xml中增加resilience4j熔断组件依赖

<dependency> 
<groupId>org.springframework.cloud</groupId> 
<artifactId>spring-cloud-starter-circuitbreaker- 
resilience4j</artifactId> 
</dependency> 

2.开启Feign的熔断器⽀持

在application.yml中增加如下配置。

feign: 
  circuitbreaker: 
    enabled: true

3.Feign熔断降级类

        Spring Cloud CircuitBreaker⽀持降级概念,当熔断器打开,或者调⽤是出现错误,则执⾏降级⽅法。@FeignClient的fallback属性指定讲解的类,注意服务降级类需要在spring容器中注册。代码如下。

package com.lxs.demo.client; 
​
import com.lxs.entity.Payment; 
import org.springframework.cloud.openfeign.FallbackFactory; 
import org.springframework.cloud.openfeign.FeignClient; 
import org.springframework.http.ResponseEntity; 
import org.springframework.stereotype.Component; 
import org.springframework.web.bind.annotation.GetMapping; 
import org.springframework.web.bind.annotation.PathVariable; 
​
@FeignClient(name = "cloud-payment-service", fallback = PaymentClient.Fallback.class) 
public interface PaymentClient { 
​
    @GetMapping("/payment/{id}") 
    public Payment payment(@PathVariable("id") Integer id); 
​
    @Component 
    static class Fallback implements PaymentClient { 
        @Override 
        public Payment payment(Integer id) { 
            Payment payment = new Payment(id, "熔断降级⽅法返回"); 
            return payment; 
        } 
    } 
} 

        如果想要获得熔断降级的异常信息,⽐如打印异常⽇志,则可以使⽤fallbackFactory属性指定。代码如下。

package com.lxs.demo.client; 
​
import com.lxs.entity.Payment; 
import org.springframework.cloud.openfeign.FallbackFactory; 
import org.springframework.cloud.openfeign.FeignClient; 
import org.springframework.http.ResponseEntity; 
import org.springframework.stereotype.Component; 
import org.springframework.web.bind.annotation.GetMapping; 
import org.springframework.web.bind.annotation.PathVariable; 
​
//@FeignClient(name = "cloud-payment-service", fallback = PaymentClient.Fallback.class) 
@FeignClient(value = "cloud-payment-service", fallbackFactory = PaymentClient.FallBackFactory.class) 
public interface PaymentClient { 
​
    @GetMapping("/payment/{id}") 
    public Payment payment(@PathVariable("id") Integer id); 
​
    @Component 
    static class Fallback implements PaymentClient { 
        @Override 
        public Payment payment(Integer id) { 
        Payment payment = new Payment(id, "熔断降级⽅法返回"); 
        return payment; 
        } 
    }
​
    @Component 
    static class FallBackFactory implements FallbackFactory<Fallback> { 
        @Override 
        public Fallback create(Throwable cause) { 
        cause.printStackTrace(); 
        return new Fallback(); 
        } 
    } 
​
} 

4.启动并测试

        启动订单服务和Eureka,此时因为没有服务提供者⽀付服务。执⾏是发⽣异常,熔断降级⽅法执⾏。

3.4.4 请求和响应压缩

可以使⽤如下配置对Feign请求和响应进⾏压缩,代码如下。

feign: 
compression: 
request: 
enabled: true # 请求压缩 
mime-types: text/xml,application/xml,application/json # 压缩的类型 
min-request-size: 2048 # 请求最⼩压缩的阈值 
response: 
enabled: true #响应压缩 
useGzipDecoder: true #使⽤gzip解码器解码响应数据 

3.4.5 Feign⽇志

        可以配置打开Feign⽇志,显示Feign调⽤的详细信息,⽐如请求和响应的headers、body和metadata。具体步骤如下。

1.设置⽇志级别

Feign Logging只响应debug级别,在application.yml中配置如下。

logging: 
  level: 
    com.lxs: debug 

2.配置FeignLoggerLevel

在配置类中配置Logger.Level,告诉配置类Feign需要打印的内容,具体代码如下。

@Configuration 
public class FooConfiguration { 
    @Bean 
    Logger.Level feignLoggerLevel() { 
    return Logger.Level.FULL; 
    } 
} 

其中Logger.Level取值如下。

  • NONE,⽆⽇志记录(默认)。

  • BASIC, 只记录请求⽅法和 URL 以及响应状态码和执⾏时间。

  • HEADERS, 记录基本信息以及请求和响应标头。

  • FULL, 记录请求和响应的标头、正⽂和元数据。

3.4.6 源码解析

1.⾃动配置

        FeignAutoConfiguration,启⽤了两个配置类:FeignClientProperties和 FeignHttpClientProperties。

@Configuration(proxyBeanMethods = false) 
@ConditionalOnClass(Feign.class) 
@EnableConfigurationProperties({ FeignClientProperties.class, 
FeignHttpClientProperties.class }) 
@Import(DefaultGzipDecoderConfiguration.class) 

public class FeignAutoConfiguration { 
...
} 

        defaultToProperties就是说默认配置⽂件的优先级更⾼,config是⼀个map,默认的key是 default,对所有的feign客户端都⽣效,可以单独指定⼀个feign客户端的名字对其做配置。

@ConfigurationProperties("feign.client") 
public class FeignClientProperties { 
    private boolean defaultToProperties = true; 
    private Map<String, FeignClientConfiguration> config = new HashMap<>(); 
    private String defaultConfig = "default"; 
}

        再看下FeignHttpClientProperties。

@ConfigurationProperties(prefix = "feign.httpclient") 
public class FeignHttpClientProperties { 

}

        回到FeignAutoConfiguration,configurations是注⼊系统中所有的FeignClientSpecification,这个类代表的是⼀个feign客户端,⾥⾯有名字和配置类,每⼀个feign客户端都会有⼀个与之对应的FeignClientSpecification。

 @Autowired(required = false) 
    private List<FeignClientSpecification> configurations = new ArrayList<>(); 
    @Bean 
    public FeignContext feignContext() { 
        FeignContext context = new FeignContext(); 
        context.setConfigurations(this.configurations); 
        return context; 
    } 

        当执⾏FeignContext的setConfiguration()的时候,实际上就是把所有的feign客户端配置类实例保存到configurations成员map中,key是client的名字,value是client本身。

private Map<String, C> configurations = new ConcurrentHashMap<>(); 
public void setConfigurations(List<C> configurations) { 
    for (C client : configurations) { 
    this.configurations.put(client.getName(), client); 
    } 
}

        这个类还有⼀个⾮常核⼼的⽅法createContext()。

protected AnnotationConfigApplicationContext createContext(String name) { 
    AnnotationConfigApplicationContext context = new 
    AnnotationConfigApplicationContext(); 
    //判断configurations是否包含那个client 
    if (this.configurations.containsKey(name)) { 
        //如果包含,拿到client的所有的配置类,注册到spring容器 
        for (Class<?> configuration : this.configurations.get(name) 
        .getConfiguration()) { 
        context.register(configuration); 
        } 
    }

    //注册default配置, 每⼀个feign客户端的context中都会有默认配置 
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) { 
        if (entry.getKey().startsWith("default.")) { 
            for (Class<?> configuration : entry.getValue().getConfiguration()) { 
            context.register(configuration); 
            } 
        } 
    }

    //然后注册PropertyPlaceholderAutoConfiguration和defaultConfigType 
    context.register(PropertyPlaceholderAutoConfiguration.class, 
    this.defaultConfigType); 

    // 然后注册了个PropertySource,名字是feign, 
    // ⾥⾯是有⼀个kv,key是feign.client.name,value是name,就是feign客户端的名字 
    context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( 
    this.propertySourceName, 
    Collections.<String, Object>singletonMap(this.propertyName, name))); 

    if (this.parent != null) { 
        // 这⾥设置了当前context的⽗context 
        context.setParent(this.parent);01 
        context.setClassLoader(this.parent.getClassLoader()); 
    }

    //设置context的名字是FeignContext-feign客户端的名字 
    context.setDisplayName(generateDisplayName(name)); 
    context.refresh(); 
    return context; 

} 

        也就是说每⼀个feign客户端都有⼀个AnnotationConfigApplicationContext,这个context⾥⾯注册了这个feign客户端的⾃⼰的配置类、全局默认的配置类、FeignClientsConfiguration这个配置类、⽤于占位符解析的配置类,添加了环境变量feign.client.name = feign客户端的名字,设置了⽗类Context和本Context的名字。

        FeignContext构造函数中传递了FeignClientsConfiguration配置类,这个类定义了对于Web解析的组件,源码如下。

public class FeignContext extends 
NamedContextFactory<FeignClientSpecification> { 
    public FeignContext() { 
    super(FeignClientsConfiguration.class, "feign", "feign.client.name"); 
    } 
} 
@Configuration 
public class FeignClientsConfiguration { 

    //这是响应解码器 
    @Bean 
    @ConditionalOnMissingBean 
    public Decoder feignDecoder() { 
        return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))); 
    }

    //这是请求编码器 
    @Bean 
    @ConditionalOnMissingBean 
    public Encoder feignEncoder() { 
    	return new SpringEncoder(this.messageConverters); 
    }

    //这是解析注解⽤的Contract 
    @Bean 
    @ConditionalOnMissingBean 
    public Contract feignContract(ConversionService feignConversionService) { 
    	return new SpringMvcContract(this.parameterProcessors, feignConversionService); 
    }

    //ConversionService 
    @Bean 
    public FormattingConversionService feignConversionService() { 
        FormattingConversionService conversionService = new 
        DefaultFormattingConversionService(); 
        for (FeignFormatterRegistrar feignFormatterRegistrar : 
        feignFormatterRegistrars) { 
        feignFormatterRegistrar.registerFormatters(conversionService); 
        }
        return conversionService; 
    }

    // 这是创建HystrixFeign.builder, 
    // 只有没有Feign.Builder并且启⽤了hystrix才会创建 
    @Configuration 
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) 
    protected static class HystrixFeignConfiguration { 
        @Bean 
        @Scope("prototype") 
        @ConditionalOnMissingBean 
        @ConditionalOnProperty(name = "feign.hystrix.enabled") 
        public Feign.Builder feignHystrixBuilder() { 
        	return HystrixFeign.builder(); 
        } 
    }

    // 永远不重试 
    @Bean 
    @ConditionalOnMissingBean 
    public Retryer feignRetryer() { 
    return Retryer.NEVER_RETRY; 
    }
    ...

} 

2.注册FeignClient

        ⾸先从@EnableFeignClient注解开始

@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.TYPE) 
@Documented 
@Import(FeignClientsRegistrar.class) 
public @interface EnableFeignClients { 

} 

        @Import中FeignClientsRegistrar完成注册,这⾥⾯主要就⼲了2件事,注册默认的配置和注册 feign客户端,当然在注册客户端的同时也会注册客户端上的⾃定义的配置类。先看下 registerDefaultConfiguration:

class FeignClientsRegistrar 
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, 
EnvironmentAware { 

    @Override 
    public void registerBeanDefinitions(AnnotationMetadata metadata, 
    BeanDefinitionRegistry registry) {
    // 注册默认的配置 
    registerDefaultConfiguration(metadata, registry); 
    // 注册feign客户端 
    registerFeignClients(metadata, registry); 
    } 

} 

注册feign客户端本身registerFeignClient。

private void registerFeignClient(BeanDefinitionRegistry registry, 
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { 
String className = annotationMetadata.getClassName(); 
//实际注册到Spring容器的类型是FeignClientFactoryBean 
BeanDefinitionBuilder definition = BeanDefinitionBuilder 
.genericBeanDefinition(FeignClientFactoryBean.class); 
validate(attributes); 
definition.addPropertyValue("url", getUrl(attributes)); 
definition.addPropertyValue("path", getPath(attributes)); 
String name = getName(attributes); 
//设置name 
definition.addPropertyValue("name", name); 
String contextId = getContextId(attributes); 
//设置contextId 
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(); 
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); 
// has a default, won't be null 
boolean primary = (Boolean) attributes.get("primary"); 
//primary默认是true,这⾥设置了primary 
beanDefinition.setPrimary(primary); 
// 设置alias 
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); 
} 

3.注⼊FeignClient

        容器启动的时候,⾸先是⾛@EnableFeignClients去注册默认的配置类、注册FeignClient和FeignClient的配置类,然后⾛@FeignAutoConfiguration,创建FeignContext,把配置类都放进去。当@Autowired注⼊feign客户端的时候,实际注⼊的是FactoryBean的getObject()返回的那个类,我们来看下。

class FeignClientFactoryBean 
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware 
{ 
@Override 
public Object getObject() throws Exception { 
return getTarget(); 
}

<T> T getTarget() { 
//先去拿到FeignContext 
FeignContext context = 
this.applicationContext.getBean(FeignContext.class); 
//构造builder 
Feign.Builder builder = feign(context); 
//如果没有配置url,⾛负载均衡 
if (!StringUtils.hasText(this.url)) { 
if (!this.name.startsWith("http")) { 
this.url = "http://" + this.name; 
}
else { 
this.url = this.name; 
}
this.url += cleanPath(); 
return (T) loadBalance(builder, context, 
new HardCodedTarget<>(this.type, this.name, this.url)); 
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { 
this.url = "http://" + this.url; 
}
String url = this.url + cleanPath(); 
//获取⼀个Client,默认是没有设置的 
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(); 
}
if (client instanceof FeignBlockingLoadBalancerClient) { 
// not load balancing because we have a url, 
// but Spring Cloud LoadBalancer is on the classpath, so unwrap 
client = ((FeignBlockingLoadBalancerClient) client).getDelegate(); 
}
builder.client(client); 
}
//创建target,这⾥⾯就是feign⾃⼰去创建动态代理了 
Targeter targeter = get(context, Targeter.class); 
return (T) targeter.target(this, builder, context, 
new HardCodedTarget<>(this.type, this.name, url)); 
} 
} 

FeignContext在FeignAutoConfiguration⾥⾯已经注册到容器了,看下是如何构造那个builder的。

protected Feign.Builder feign(FeignContext context) { 
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); 
Logger logger = loggerFactory.create(this.type); 
// @formatter:off 
//拿到Feign.Builder 
Feign.Builder builder = get(context, Feign.Builder.class) 
// required values 
.logger(logger) 
//encoder 
.encoder(get(context, Encoder.class)) 
//decoder 
.decoder(get(context, Decoder.class)) 
//contract 
.contract(get(context, Contract.class)); 
// @formatter:on 
configureFeign(context, builder); 
return builder; 
} 

这个其实就是拿到feign客户端的name对应的那个ApplicationContext,因此,最终就是从这个Context中去拿Type,如果这个Context不存在就去创建,前⾯已经分析过了,创建的时候回去注册很多配置类,其中有⼀个FeignClientsConfiguration,这⾥会创建默认的encoder、decoder⼀⼤堆。

4.总结:

(1)@EnableFeignClients会做bean扫描,向Spring容器注册全局默认配置、FeignClient、 FeignClient配置,其中FeignClient不是普通的Bean,⽽是⼀个FeignClientFactoryBean

(2)FeignAutoConfiguration向Spring容器注册了FeignContext,FeignContext⾥⾯包含了所有的FeignClient,每⼀个FeignClient都关联了⼀个ApplicationContext,这个ApplicationContext中就包含了encoder、decoder、contract那些核⼼组件,SpringMvcContract就是⽤来解析web相关注解的。

(3)当@Autowired注⼊FeignClient接⼝的时候,实际注⼊的是FeignClientFactoryBean的getObject()返回的bean,在getObject()⾥⾯调⽤了buidler.target()返回了FeignClient实例。

(4)如果在⼦线程中调⽤feign接⼝,需要注意⼦线程中是⽆法获取HttpServletRequest的,此时就算是feign接⼝配置了拦截器,在拦截器⾥⾯⼀样是⽆法读取到http header的,对于某些使⽤拦截器统⼀设置http header的情况尤其要注意,feign说⽩了就是个http util,仅此⽽已。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13439.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!