https://www.cnblogs.com/qlqwjy/p/15594642.html
详解spring cloud Feign使用中遇到的问题总结(不能用@GetMapping或@PostMapping)_feign getmapping_HD243608836的博客-CSDN博客
白话:
1.RestTemplate底层采用Httpclient实现,可以配合ribbon的@LoadBalanced注解使用,实现负载均衡调用。
2.@LoadBalanced注解使用了@Qualifier注解,在@Autowire注入的时候可以将bean归类注入。
3.Ribbon相关配置类中使用到了该注解获取resttemplate集合,并且通过SmartInitializingSingleton的afterSingletonsInstantiated方法和restTemplateCustomizer,为resttemplate设置拦截器,之后resttemplate每次进行调用的使用都会经过该拦截器,进行Ribbon负载均衡的处理,最后通过HttpClient发送请求。
4.Feign、OpenFeign底层都集成了ribbon和Httpclient,Feign一般是指spring-cloud一代(netflix)中的组件,不支持Spring中的一些注解,比如PostMapping、GetMapping等;
OpenFeign是spring-cloud二代(alibaba)中的组件,支持Spring注解。
Ribbon简介
什么是Ribbon?
Ribbon是Netflix发布的负载均衡器,它可以帮我们控制HTTP和TCP客户端的行为。只需为Ribbon配置服务提供者地址列表,Ribbon就可基于负载均衡算法计算出要请求的目标服务地址。
Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机、响应时间加权等——当然,为Ribbon自定义负载均衡算法也非常容易,只需实现IRule
接口即可。
SpringCloud提供了Ribbon用来做客户端负载均衡,通过SpringCloud对Rbbon的封装,我们可以很轻松的通过负载均衡去调用我们开发的rest服务,不需要手动去处理因负载均衡而出现的各种棘手情况,
Ribbon并不需要像eureka和网关那样单独部署,它是和每一个微服务耦合在一起的。
使用resttemplate与ribbon整合去实现负载均衡的调用,启用方式是需要我们在RestTemplate 实例配置上面添加@LoadBalanced注解。
可以去了解一下RestTemplate的api使用。
怎么用?
加依赖
基于已经有的SpirngCloud项目,由于spring-cloud-starter-netflix-eureka-client
已经包含 spring-cloud-starter-netfilx-ribbon
,故而无需额外添加依赖。
写代码
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
只需在RestTemplate
上添加LoadBalanced
注解,即可让RestTemplate整合Ribbon!
调用
@GetMapping("/users/{id}") public User findById(@PathVariable Long id) { // 这里用到了RestTemplate的占位符能力 User user = this.restTemplate.getForObject( "http://microservice-provider-user/users/{id}", User.class, id ); // ...业务... return user; }
我们将请求的目标服务改成了http://microservice-provider-user/users/{id}
,也就是http://{目标服务名称}/{目标服务端点}
的形式,Ribbon会自动在实际调用时,将目标服务名替换为该服务的IP和端口。
WARNING
事实上,这里的目标服务名称,在Ribbon里叫虚拟主机名
,主机名是不能包含_
等特殊字符的——这意味着,一般不建议配置spring.application.name = xxx_xxx
,如果你的应用名称一定带有下划线这种字符,
那么请额外配置eureka.instance.virtual-host-name = 一个合法的主机名
,否则Ribbon将会提示虚拟主机名不合法的异常(在早期的版本则是报空指针)!
什么时候用Ribbon,用在哪?
上面知道Ribbon实现在客户端的负载均衡,所以当我们需要调用多台部署了相同项目的服务器时,就可以使用Ribbon。既然时客户端的负载均衡,那自然是在调用方使用了。
比如服务调用方A调用服务提供方B/C/D,就可以再A使用Ribbon,负载均衡的调用BCD。
深入
那么Ribbon是如何实现负载均衡的呢?
根据我们前面说Ribbon实现的的客户端负载均衡,所以它自己肯定有一个可用的服务列表,服务列表里面存储的是可用服务的地址,这是第一个条件;
第二个是我们在RestTemplate上面添加了注解后,它就自动实现了负载均衡,这个我们可以想到它拦截了我们的请求,所以这里肯定会有一个拦截器在帮助我们实现此功能;
第三个是为什么我们加上了注解后,也可以使用服务名直接去调用了呢,肯定也有组件帮我们实现了替换的
看源码!!
因为时通过注解@LoadBalanced开启的Ribbon,所以就从这个注解开始看。
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
可以看到类里面是空的。那想到在springboot中,有一个申明式的注解,必定在其同名的包下面会有一个这样的(xxxAutoConfiguration)配置类,去配置这个注解,
我们定位到该包下面,看到同名的包下面确实有这样一个配置类LoadBalancerAutoConfiguration
点进去是这样的:
@Configuration(//@Configuration:注解可以用java代码的形式实现spring中xml配置文件配置的效果。 proxyBeanMethods = false ) @ConditionalOnClass({RestTemplate.class})//@ConditionalOnClass:其用途是判断当前classpath下是否存在指定类,若是则将当前的配置装载入spring容器 @ConditionalOnBean({LoadBalancerClient.class})//@ConditionalOnBean:当给定的在bean存在时,则实例化当前Bean @EnableConfigurationProperties({LoadBalancerRetryProperties.class}) public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired( required = false ) //这里面会注入有@loadbalence注解的所有的restTemplate实例 private List<RestTemplate> restTemplates = Collections.emptyList(); @Autowired( required = false ) //还有很多。。。 }
@EnableConfigurationProperties:如果该类只使用了@ConfigurationProperties注解,然后该类没有在扫描路径下或者没有使用@Component等注解,导致无法被扫描为bean,那么就必须在配置类上使用@EnableConfigurationProperties注解去指定这个类,这个时候就会让该类上的@ConfigurationProperties生效,然后作为bean添加进spring容器中
再点击RestTemplate进去,看到此类继承了
InterceptingHttpAccessor
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations { //。。。。 }
看来真的有拦截器在起作用,我们可以看到InterceptingHttpAccessor类中有一个setInterceptors方法
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) { Assert.noNullElements(interceptors, "'interceptors' must not contain null elements"); if (this.interceptors != interceptors) { this.interceptors.clear(); this.interceptors.addAll(interceptors); AnnotationAwareOrderComparator.sort(this.interceptors); } }
然后我们看看他是在哪里调用这份方法的,我们再回到LoadBalancerAutoConfiguration配置类中,看到这段方法:
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) { return (restTemplate) -> { List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
看到这里在往resttemplate中添加拦截器loadBalancerInterceptor,我们点击这个拦截器进去,主要看这段代码
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI();//获取URI String serviceName = originalUri.getHost();//获取serviceName 就是每个微服务的应用名称 Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); LoadBalancedRetryPolicy retryPolicy = this.lbRetryFactory.createRetryPolicy(serviceName, this.loadBalancer); RetryTemplate template = this.createRetryTemplate(serviceName, request, retryPolicy); return (ClientHttpResponse)template.execute((context) -> { ServiceInstance serviceInstance = null; if (context instanceof LoadBalancedRetryContext) { LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context; serviceInstance = lbContext.getServiceInstance(); } if (serviceInstance == null) { serviceInstance = this.loadBalancer.choose(serviceName);//查看这个choose方法,这里通过负载均衡选择一个server } ClientHttpResponse response = (ClientHttpResponse)this.loadBalancer.execute(serviceName, serviceInstance, this.requestFactory.createRequest(request, body, execution)); int statusCode = response.getRawStatusCode(); if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) { byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody()); response.close(); throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy); } else { return response; } }, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() { protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) { return response; } });
查看choose方法:
public ServiceInstance choose(String serviceId, Object hint) { Server server = this.getServer(this.getLoadBalancer(serviceId), hint);//通过serviceID找到对应的服务实例均衡器,loadBalancer 这里面保存了所有的服务实例,可用的,宕机的都在里面 return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)); }
使用均衡算法拿到可用的服务实例(从几个中选择一个,达到均衡的作用),返回server,然后拿到这个server就可以继续执行目标请求。
均衡器里面会保存allServerList,里面会有upServeLlist里面是我们启动的服务实例,然后使用负载均衡算法选择一个服务。
ribbon的常见配置
1.禁用eureka
当我们在resttemplate上面添加loadbalence注解后,就可以使用服务名去调用,如果我们想关闭这个功能,可以使用ribbon.eureka.enable=false
2.配置接口地址列表
如果我们关闭了eureka之后,还想用服务名去调用,就需要手动配置服务配置列表
服务名.ribbon.listOfServers=IP:PORT1,IP:PORT2
3.配置负载均衡策略
服务名.ribbon.NFLoadBalencerRuleClassName=策略class全类名
4.超时时间
ribbon中有两种和超时时间相关的配置
ribbon.ConnectTimeout=2000 请求连接的超时时间
ribbon.ReadTimeout=5000 请求处理的超时时间
可以在前面加上具体的服务名,为指定的服务配置
5.并发参数
ribbon.MaxTotalConnections=500 最大连接数
ribbon,MaxConnectionsPerHost=500 每个host最大连接数
参考文档
https://www.cnblogs.com/sglx/p/15735352.html
Ribbon源码 --- 服务列表的初始化和刷新_ribbonloadbalancerclient 手动刷新serverlist_sctttt的博客-CSDN博客