前言
代码分析基于版本如下:
<java.version>1.8</java.version>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
OpneFeign
OpneFeign其实就是fegin的封装使得我们可以像编写Spring MVC编写一个Http调用,而feign就是对http客户端封装底层屏蔽了httpclient客户端实现,即可随时进行替换。
用法
声明式
通过@FeignClient标注接口即声明为FeignClient
@FeignClient(value = "${user.app.name}", fallback = UserSecurityServiceFeignFallback.class)
public interface UserSecurityServiceFeign {
/**
* 验证token,返回对应的权限信息
*
* @param authenticateTokenCmd token身份验证命令
* @return 登录
*/
@PostMapping("/auth/token/authentication")
ResultDTO<UserPermissionInfoCO> authenticateToken(AuthenticateTokenCmd authenticateTokenCmd);
}
继承式
feigin允许继承,通过继承我们可以覆盖一些配置进行自定义FeignClient。例如UserSecurityServiceFeign在API module中,然后在其他module我们可以引用API module然后定义一个FeignClient来继承它,然后进行自定义。
@FeignClient(value = "${user.app.name}", fallback = UserSecurityServiceFeignFallback2.class)
public interface SelfUserSecurityServiceFeign extends UserSecurityServiceFeign{
}
原理分析
入口
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//需要扫描的包
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
//默认配置例如feign.codec.Decoder 、 feign.codec.Encoder 、 feign.Contract 这些。
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
从@EnableFeignClients开始,其用@Import注解导入了FeignClientsRegistrar。FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar也就意味着在BeanFactoryPostProcessor进行配置类解析时,会执行registerBeanDefinitions方法去注册@FeignClient对应的BeanDefintion。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册默认的feign配置类
registerDefaultConfiguration(metadata, registry);
//注册FeignClients
registerFeignClients(metadata, registry);
}
注册默认的feign配置类
可以看到根据@EnableFeignClients注解的defaultConfiguration值,构成一个FeignClientSpecificationBeanDefinition然后注册到容器中
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"));
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition());
}
注册FeignClients-类扫描注册
首先通过类扫描指定的包路径,找到带@FeignClient注解的,然后注册为FeignClientFactoryBean,它在依赖注入返回的是其getObject方法返回对象-代理对象。
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
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) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
//设置过滤器 扫描带@FeignClient注解的
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
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"));
//注册为FeignClientFactoryBean
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
Feign配置
可以看到都是从容器中获取对应功能模块builder中(没有则是builder中默认,而builder是原型Bean),所以我们要替换默认的功能时注册一个配置覆盖即可。
protected void configureUsingConfiguration(FeignContext context,
Feign.Builder builder) {
//日志
Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class);
if (level != null) {
builder.logLevel(level);
}
//重试机制
Retryer retryer = getInheritedAwareOptional(context, Retryer.class);
if (retryer != null) {
builder.retryer(retryer);
}
//错误编码
ErrorDecoder errorDecoder = getInheritedAwareOptional(context,
ErrorDecoder.class);
if (errorDecoder != null) {
builder.errorDecoder(errorDecoder);
}
else {
FeignErrorDecoderFactory errorDecoderFactory = getOptional(context,
FeignErrorDecoderFactory.class);
if (errorDecoderFactory != null) {
ErrorDecoder factoryErrorDecoder = errorDecoderFactory.create(type);
builder.errorDecoder(factoryErrorDecoder);
}
}
Request.Options options = getInheritedAwareOptional(context,
Request.Options.class);
if (options != null) {
builder.options(options);
readTimeoutMillis = options.readTimeoutMillis();
connectTimeoutMillis = options.connectTimeoutMillis();
}
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(
context, RequestInterceptor.class);
if (requestInterceptors != null) {
List<RequestInterceptor> interceptors = new ArrayList<>(
requestInterceptors.values());
AnnotationAwareOrderComparator.sort(interceptors);
builder.requestInterceptors(interceptors);
}
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context,
QueryMapEncoder.class);
if (queryMapEncoder != null) {
builder.queryMapEncoder(queryMapEncoder);
}
if (decode404) {
builder.decode404();
}
ExceptionPropagationPolicy exceptionPropagationPolicy = getInheritedAwareOptional(
context, ExceptionPropagationPolicy.class);
if (exceptionPropagationPolicy != null) {
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
}
}
注册为FeignClientFactoryBean
最为核心的一步,BeanDefintion注册到Spring 容器中我们才能依赖注入FeignClient。读取其@FeignClient的配置元数据attributes生成一个个FeignClientFactoryBean-BeanDefinition注册到容器中。
//attributes @FeignClient中读取的配置信息
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
//重点!!!BeanDefinition类型为FeignClientFactoryBean
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);
//type 设置的FactoryBean 返回的 类型
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");
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);
}
FeignClient的生成
根据上面的注册FeignClient的代码,可知FeignClient所注册的BeanDefinition是FeignClientFactoryBean一个FactoryBean实现类,也就是在依赖注入时注入的是其getObject方法所返回的对象。源码里可以看到FeignClient是一个单例bean,最终由Feign.Builder创建Feign,再利用Feign(ReflectiveFeign)来创建一个JDK动态代理对象-也就是FeignClient 实例。
@Override
public Object getObject() throws Exception {
return getTarget();
}
//创建代理对象
<T> T getTarget() {
//1、Feign上下文容器 并配置到Feign.Builder中 FeignContext 对于每个服务都有一个子容器
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
//2、如果没有配置url则配置时服务名需要进行负载均衡
if (!StringUtils.hasText(url)) {
//且name中不包含http
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
// HardCodedTarget 为硬编码访问的对象
//使用负载均衡client
return (T) loadBalance(builder, context,new HardCodedTarget<>(type, name, url));
}
//...省略url拼接
Client client = getOptional(context, Client.class);
//此时有完整的url则需要去掉负载均衡的功能 即直接拿到装饰模式内部的Client
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
//targeter创建client target方法最终返回的都是feign.target tartet类型是@FeignClient的Interface
return (T) targeter.target(this, builder, context,new HardCodedTarget<>(type, name, url));
}
@Override
public boolean isSingleton() {
//feignClient是单例
return true;
}
//Feign.builder 的target方法 创建一个动态代理对象
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
//负载均衡
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,HardCodedTarget<T> target) {
//上下文容器中获取Client
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?");
}
Feign.Builder!!!
feign的构建器,它是feign的核心决定了Feigin的构建方式(也确定了整体逻辑流程),通过它可以进行扩展例如整合Hystrix、sentinel都是暴露一个Feign.BuilderBean来整合自己的逻辑。可以看到Feign.Builder确实是从容器中获取的(FeignContext是NamedContextFactory)。
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(type);
// 从容器中获取builder
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
// @formatter:on
configureFeign(context, builder);
return builder;
}
//Feign.Builder 所预留的扩展
public static class Builder {
//RequestInterceptor 请求拦截器
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
//方法上注解解析
private Contract contract = new Contract.Default();
//client 不同功能的客户端 例如负载均衡
private Client client = new Client.Default(null, null);
//重试机制
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
//解码和编码
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
//异常处理
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
//核心:生成代理对象的拦截器 FeignClient 时JDKdongd
private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default();
private boolean decode404;
private boolean closeAfterDecode = true;
private ExceptionPropagationPolicy propagationPolicy = NONE;
private boolean forceDecoding = false;
private List<Capability> capabilities = new ArrayList<>();
public Feign build() {
//其他代码省略
//生成Feign
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}
RequestInterceptor
请求拦截器,非常重要的扩展
动态代理
ReflectiveFeign中创建了JDK代理对象,根据方法上的注解信息(Contract解析)创建MethodHandler,在InvocationHandler中根据Method对应的MethodHandler去执行对应的逻辑。
//目标
public <T> T newInstance(Target<T> target) {
//解析方法上注解创建对应的MethodHandler
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()) {
//Oject中方法忽略
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 feign中InvocationHandlerFactory
InvocationHandler handler = factory.create(target, methodToHandler);
//JDK动态代理
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
InvocationHandlerFactory-创建代理对象增强逻辑
JDK动态代理InvocationHandler工厂,扩展时需要覆盖其Feign.Bulider中invocationHandlerFactory。
public interface InvocationHandlerFactory {
//dispatch 分发根据Method找对对应的MethodHandler
InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);
interface MethodHandler {
Object invoke(Object[] argv) throws Throwable;
}
static final class Default implements InvocationHandlerFactory {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
//创建的jdk代理增量逻辑
return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
}
}
}
ReflectiveFeign.FeignInvocationHandler-默认增强逻辑
根据方法找到对应的MethodHandler,然后执行
//ReflectiveFeign.FeignInvocationHandler 增量
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
FeignContext
FeignContext为之执行上下文。FeignContext继承了NamedContextFactory,也就是它为每个客户端都创建一个子ApplicationContext,并从这个ApplicationContext中获取需要的配置Bean-FeignClientSpecification来实现配置隔离。它实现了ApplicationContextAware这一Bean生命周期接口,所以能够拿到父容器组成父子容器。即每个服务(contextId)创建一个子容器和Spring boot构成父子容器。然后获取bean时先从子容器中获取,然后在从父容器中获取,以子容器的为主同时存在的话。(spring 默认的父子容器时优先父容器中获取,父容器获取不到再子容器)。通过NamedContextFactory来隔离不同客户端的配置。
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {...}
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
// name 为客服端名称,也就是服务名 获取子容器
protected AnnotationConfigApplicationContext getContext(String name) {
//double check+synchronized 保证每个client对应的ApplicationContext只有一个 参考单例模式
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
//如果没有则创建加入map中
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
//省略其他
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
//拿到父容器
this.parent = parent;
}
public <T> T getInstance(String name, ResolvableType type) {
AnnotationConfigApplicationContext context = getContext(name);
//获取符合类型的bean
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type);
if (beanNames.length > 0) {
for (String beanName : beanNames) {
if (context.isTypeMatch(beanName, type)) {
return (T) context.getBean(beanName);
}
}
}
return null;
}
}
private static String[] mergeNamesWithParent(String[] result, String[] parentResult, HierarchicalBeanFactory hbf) {
if (parentResult.length == 0) {
return result;
}
List<String> merged = new ArrayList<>(result.length + parentResult.length);
merged.addAll(Arrays.asList(result));
for (String beanName : parentResult) {
//子容器中不存在
if (!merged.contains(beanName) && !hbf.containsLocalBean(beanName)) {
merged.add(beanName);
}
}
return StringUtils.toStringArray(merged);
}
NamedContextFactory!-配置隔离
通过子容器来隔离配置(C extends NamedContextFactory.Specification),实现原理是一个ConcurrentHashMap存储不的子容器,子容器存储NamedContextFactory.Specification,且子容器的优先级高于父容器。Specification存储配置类数组(即不实例化配置),当创建子容器的时候,将这些配置类注册当子容器中,在子容器中初始化。
所以不要把那些配置类直接注册到主容器中,否则会变成全局配置 因为父子容器getBean优先从父容器中获取。
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
//子容器缓存 key
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
private Map<String, C> configurations = new ConcurrentHashMap<>();
//父容器
private ApplicationContext parent;
private Class<?> defaultConfigType;
//存储配置类的实现
public interface Specification {
String getName();
//隔离的配置类
Class<?>[] getConfiguration();
}
//获取子容器
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
}
创建子容器
如果容器不存在创建一个新的AnnotationConfigApplicationContext,将自动装配得到的对应服务名和默认配置的Specification(配置类数组)中的配置注册进这个子ApplicationContext,然后setParent组成具有父子层次ApplicationContext,最后启动容器context.refresh()。可以看到顺序是先服务名字指定的配置再默认配置(default.开头)。由于@ConditionalOnMissingBean注册顺序决定了配置优先级。
一般这些配置都带有
@ConditionOnMissBean,所以是精确配置大于默认配置
// 创建指定服务名称对应的内置容器
protected AnnotationConfigApplicationContext createContext(String name) {
// 创建容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 如果配置类上含有指定应用名称的键值对,那么将此将Specification中configuration 类都注册到内置容器中
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
// 配置类的key 是否是 "default." 开头的,如果是,那么也注册到服务对应的内置容器中
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
// 将Specification中configuration 类注入的容器中
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 注册默认的配置和环境变量替换器
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object> singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
}
// 刷新容器,实例化所有Bean
context.refresh();
return context;
}
获取实例
交给对应的子容器取获取对应类型的Bean实例,而对于Spring 父子容器来说优先从父容器中查找,父容器没有才从子容器查找,类似于双亲委派模型。
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
try {
return context.getBean(type);
}
catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
Client
http客户端抽象封装,fegin默认情况下使用HttpURLConnection,HttpURLConnection是不支持连接池的,
public interface Client {
/**
* Executes a request against its {@link Request#url() url} and returns a response.
*
* @param request safe to replay.
* @param options options to apply to this request.
* @return connected response, {@link Response.Body} is absent or unread.
* @throws IOException on a network error connecting to {@link Request#url()}.
*/
Response execute(Request request, Options options) throws IOException;
}
如何使用其他http客户端
引用下面其中一个依赖即可使用对应的http客户端
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 或者添加 httpclient 框架依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
以Apache httpclient为例
在自动装配中FeignAutoConfiguration,当存在ApacheHttpClient上面jar引入的时候,则配置连接池以及client。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer(
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
@Autowired(required = false)
private RegistryBuilder registryBuilder;
private CloseableHttpClient httpClient;
//配置连接池
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
//配置客户端
@Bean
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build();
this.httpClient = httpClientFactory.createBuilder()
.setConnectionManager(httpClientConnectionManager)
.setDefaultRequestConfig(defaultRequestConfig).build();
return this.httpClient;
}
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
//销毁是关闭连接池
@PreDestroy
public void destroy() throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
this.httpClient.close();
}
}
}
连接池配置
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {
如何选择那种客户端
在本文上述内容中我们通过不同的客户端工具实现了发送同步的GET请求和异步的POST请求。
各个工具的特点可以总结为以下几点:
- 如果不想添加任何外部库并且应用程序的JDK版本是11+,那么原生
HTTPClient是首选; - 如果是
Spring Boot应用并且是反应式API,则使用Spring WebClient; Apache HttpClient的灵活性更高,相比其他库有更多的参考文档;OkHttpClient的性能最佳,客户端对象可重复使用,功能丰富,高度可配置。
builder详细解析
builder包含fegin中所有功能和配置。
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
private boolean decode404;
private boolean closeAfterDecode = true;
private ExceptionPropagationPolicy propagationPolicy = NONE;
private boolean forceDecoding = false;
private List<Capability> capabilities = new ArrayList<>();
重试机制-Retryer
重试机制,OpenFeign默认的重试机制是不重试。
public interface Retryer extends Cloneable {
/**
* if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception.
*/
void continueOrPropagate(RetryableException e);、
}
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
扩展
如果需要自定义Feign,我们只需要配置一个自定义Feign.Builder重新build 方法。其他扩展请查看Feign.Builder中各个属性,例如RequestInterceptor。
限流熔断和负载均衡是先限流熔断,后负载均衡
限流熔断扩展
Sentinel、Hystrix对feign的支持,主要是覆盖了invocationHandlerFactory使用了自己的SentinelInvocationHandler在feign代理对象的执行时加入Sentinel的核心代码。以下为Sentinel对feign的支持。
invocationHandler就是 JDK动态代理提供的扩展接口。
public final class SentinelFeign {
public static Builder builder() {
return new Builder();
}
//自定义Feign.Builder
public static final class Builder extends Feign.Builder implements ApplicationContextAware {
//重写build方法
@Override
public Feign build() {
//设置自定义invocationHandlerFactory
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target,Map<Method, MethodHandler> dispatch) {
//省略其他配置读取代码
return new SentinelInvocationHandler(target, dispatch);
}
//其他invocationHandlerFactory代理码
});
super.contract(new SentinelContractHolder(contract));
return super.build();
}
//省略其他
}
SentinelInvocationHandler
可以看到sentinel和feign整合的时候,资源名为 且作为出口流量。资源名称为 HttpMethod:protocol://url,而在对应需要负载均衡的TargetURL为对应的服务名:例如 GET:Http://serviceName/xxxx/xxx
//SentinelInvocationHandler 核心 代理模式拦截方法执行加入sentinel核心逻辑
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)throws Throwable {
//http方法名+url+path 作为资源
String resourceName = methodMetadata.template().method().toUpperCase()
+ ":" + hardCodedTarget.url() + methodMetadata.template().path();
Entry entry = null;
try {
//sentinel 模板代码
ContextUtil.enter(resourceName);
//出口留香
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
//具体执行逻辑
result = methodHandler.invoke(args);
}catch (Throwable ex) {
//异常处理
}
finally {
if (entry != null) {
entry.exit(1, args);
}
ContextUtil.exit();
}
//其他代码省略
return result;
}
负载均衡扩展
feign默认集成Ribbon做负载均衡,其原理是在FeignClientFactoryBean创建FeignClient时如果发现配置为服务名时则进行负载均衡。从容器中获取ClientBean对象,然后设置到Builder中。Client中包含负载均衡逻辑。对Client进行增强(装饰器模式)封装成带负载均衡功能。由于每个client都有自己的子容器FeignContext,
//判断是否需要是需要负载均衡
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
//创建负载均衡的feignClient
return (T) loadBalance(builder, context,new HardCodedTarget<>(type, name, url));
}
//负载均衡
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?");
}
//从子容器器中获取
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(contextId, type);
}
LoadBalancer
FeignLoadBalancerAutoConfiguration
feign 负载均衡自动装配,根据当前feign客户端类型装饰不同的负载均衡Client。
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
OkHttpFeignLoadBalancerConfiguration.class,
DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
}
以OkHttp为例当feign.okhttp.enabled=true,将okHttpClient先装饰为OkHttpClient在装饰为FeignBlockingLoadBalancerClient(包含负载均衡client)。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
@Conditional(OnRetryNotEnabledCondition.class)
public Client feignClient(okhttp3.OkHttpClient okHttpClient,BlockingLoadBalancerClient loadBalancerClient) {
//被装饰的OkHttpClient
OkHttpClient delegate = new OkHttpClient(okHttpClient);
//装饰为 FeignBlockingLoadBalancerClient 带有负载均衡功能
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
@ConditionalOnBean(LoadBalancedRetryFactory.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
havingValue = "true", matchIfMissing = true)
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
okhttp3.OkHttpClient okHttpClient,
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
loadBalancedRetryFactories.get(0));
}
}
可以看到FeignBlockingLoadBalancerClient对OkHttpClient装饰增加负载均衡功能。
public class FeignBlockingLoadBalancerClient implements Client {
private static final Log LOG = LogFactory
.getLog(FeignBlockingLoadBalancerClient.class);
//被装饰的
private final Client delegate;
private final BlockingLoadBalancerClient loadBalancerClient;
public FeignBlockingLoadBalancerClient(Client delegate,
BlockingLoadBalancerClient loadBalancerClient) {
this.delegate = delegate;
this.loadBalancerClient = loadBalancerClient;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
final URI originalUri = URI.create(request.url());
//进行负载均衡
String serviceId = originalUri.getHost();
Assert.state(serviceId != null,
"Request URI does not contain a valid hostname: " + originalUri);
//选择一个实例
ServiceInstance instance = loadBalancerClient.choose(serviceId);
//不能进行负载均衡
if (instance == null) {
String message = "Load balancer does not contain an instance for the service "
+ serviceId;
if (LOG.isWarnEnabled()) {
LOG.warn(message);
}
return Response.builder().request(request)
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.body(message, StandardCharsets.UTF_8).build();
}
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
.toString();
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl,
request.headers(), request.body(), request.charset(),
request.requestTemplate());
return delegate.execute(newRequest, options);
}
// Visible for Sleuth instrumentation
public Client getDelegate() {
return delegate;
}
}
Ribbon
原理分析
@RibbonClients、@RibbonClient注解
指定多个@RibbonClient,并设置这些RibbonClient的默认配置
@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
RibbonClient[] value() default {};
Class<?>[] defaultConfiguration() default {};
}
@Configuration(proxyBeanMethods = false)
@Import(RibbonClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RibbonClient {
String value() default "";
String name() default "";
Class<?>[] configuration() default {};
}
RibbonClientConfigurationRegistrar对@RibbonClient注解进行解析,将指定的配置类包装为RibbonClientSpecification(配置类的容器)并注册到主容器中(此时配置类并未被实例化)。最后依赖注入到SpringClientFactory。
@RibbonClients、@RibbonClient RibbonClientSpecification只是注册为RibbonClientSpecification,即为不同服务设置不同的配置。
//RibbonClientConfigurationRegistrar 代码解析
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata
.getAnnotationAttributes(RibbonClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client),
client.get("configuration"));
}
}
//默认配置 在名字前
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
//默认配置带上default
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
//修改为RibbonClientSpecification注册容器中
registerClientConfiguration(registry, name,
attrs.get("defaultConfiguration"));
}
Map<String, Object> client = metadata
.getAnnotationAttributes(RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
//注册容器中
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
//修改为RibbonClientSpecification
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(RibbonClientSpecification.class);
builder.addConstructorArgValue(name);
//
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".RibbonClientSpecification",
builder.getBeanDefinition());
}
SpringClientFactory-配置隔离!!!
配置信息都存放在SpringClientFactory 中。SpringClientFactory 是SpringCloud拓展NamedContextFactory,用来整合Ribbon的一个容器工厂类,且进行配置隔离(具体参考上面的NamedContextFactory)。
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(
name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
//....
@Bean
@ConditionalOnMissingBean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
@Bean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
@ConditionalOnMissingBean
public LoadBalancedRetryFactory loadBalancedRetryPolicyFactory(
final SpringClientFactory clientFactory) {
return new RibbonLoadBalancedRetryFactory(clientFactory);
}
//...
}
从上述代码上看,主要是定义了一个SpringClientFactory 的Bean,将全局配置类(上面注解解析的)填充入此Bean。然后通过SpringClientFactory 定义另外两个Bean:
- RibbonClientHttpRequestFactory : Http请求工厂,作用是通过URL和Method生成HttpRequest对象。将此工厂通过RestTemplate的自定义拓展器注入到所有标识了
@LoadBalanced的RestTemplate的Bean中。即对现有RestTemplate进行适配 - RibbonLoadBalancerClient :负载均衡客户端:从服务中选择一个服务,使用其IP地址替换之前生成的HttpRequest的服务名称。
Client装饰器-LoadBalancerFeignClient
装饰Client增加负载逻辑
public class LoadBalancerFeignClient implements Client {
private final Client delegate;
private CachingSpringLoadBalancerFactory lbClientFactory;
private SpringClientFactory clientFactory;
//...
@Override
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);
}
}
//从SpringClientFactory中获取配置
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
//创建或者获取负载均衡器
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
//...
}
配置ribbon
代码配置Ribbon
配置 Ribbon 最简单的方式就是通过配置文件实现。当然我们也可以通过代码的方式来配置。
通过代码方式来配置之前自定义的负载策略,首先需要创建一个配置类,初始化自定义的策略,代码如下所示。需要注意的是这里不能直接交给Spring管理,否则就是全局配置。
public class BeanConfiguration {
@Bean
public MyRule rule() {
return new MyRule();
}
}
创建一个 Ribbon 客户端的配置类,关联 BeanConfiguration,用 name 来指定调用的服务名称,代码如下所示。
@Configuration
@RibbonClient(name = "EUREKA-CLIENT",configuration = BeanConfiguration.class)
public class RibbonClientConfig {
}
配置文件方式配置Ribbon
除了使用代码进行 Ribbon 的配置,我们还可以通过配置文件的方式来为 Ribbon 指定对应的配置:
格式:<clientName>.<nameSpace>.<propertyName>=<value>
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter(服务列表的过滤)
clientName在feign中就是服务名称,ribbon为命名空间
配置读取原理-IClientConfig
在IClientConfig默认实现中(DefaultClientConfigImpl),根据restClientName读取restClientName前缀的properties。即再子容器中根据服务名字读取Property中对应的配置
@Override
public void loadProperties(String restClientName){
enableDynamicProperties = true;
setClientName(restClientName);
//加载默认配置
loadDefaultValues();
//读取前缀为restClientName所对应的属性
Configuration props = ConfigurationManager.getConfigInstance().subset(restClientName);
for (Iterator<String> keys = props.getKeys(); keys.hasNext(); ){
String key = keys.next();
String prop = key;
try {
if (prop.startsWith(getNameSpace())){
prop = prop.substring(getNameSpace().length() + 1);
}
//设置到IClientConfig中
setPropertyInternal(prop, getStringValue(props, key));
} catch (Exception ex) {
throw new RuntimeException(String.format("Property %s is invalid", prop));
}
}
}
具体的key查看IClientConfigKey的实现类CommonClientConfigKey例如:
//LoadBalancer Related
public static final IClientConfigKey<Boolean> InitializeNFLoadBalancer = new CommonClientConfigKey<Boolean>("InitializeNFLoadBalancer"){};
public static final IClientConfigKey<String> NFLoadBalancerClassName = new CommonClientConfigKey<String>("NFLoadBalancerClassName"){};
public static final IClientConfigKey<String> NFLoadBalancerRuleClassName = new CommonClientConfigKey<String>("NFLoadBalancerRuleClassName"){};
public static final IClientConfigKey<String> NFLoadBalancerPingClassName = new CommonClientConfigKey<String>("NFLoadBalancerPingClassName"){};
public static final IClientConfigKey<Integer> NFLoadBalancerPingInterval = new CommonClientConfigKey<Integer>("NFLoadBalancerPingInterval"){};
public static final IClientConfigKey<Integer> NFLoadBalancerMaxTotalPingTime = new CommonClientConfigKey<Integer>("NFLoadBalancerMaxTotalPingTime"){};
默认Spring-cloud-netfix-ribbon配置
在DefaultClientConfigImpl.loadDefaultValues中,例如
public void loadDefaultValues() {
//省略其他配置只贴出来了负载均衡器。
//ZoneAwareLoadBalancer
putDefaultStringProperty(CommonClientConfigKey.NFLoadBalancerClassName, getDefaultNfloadbalancerClassname());
//默认的负载规则 AvailabilityFilteringRule 但是RibbonClientConfiguration覆盖这个默认
putDefaultStringProperty(CommonClientConfigKey.NFLoadBalancerRuleClassName, getDefaultNfloadbalancerRuleClassname());
putDefaultStringProperty(CommonClientConfigKey.NFLoadBalancerPingClassName, getDefaultNfloadbalancerPingClassname());
}
负载均衡器
ILoadBalancer为负载均衡器抽象,定义负载器有哪些功能。
public interface ILoadBalancer {
//选择一个Server
public Server chooseServer(Object key);
//其他为Server列表管理
public void addServers(List<Server> newServers)
public void markServerDown(Server server);
//获取可达Server
public List<Server> getReachableServers();
public List<Server> getAllServers();
}
- 维护了存储服务实例Server对象的二个列表:一个用于存储所有服务实例的清单,一个用于存储正常服务(up服务)的实例清单
- 初始化得到可用的服务列表,启动定时任务去实时的检测服务列表中的服务的可用性,并且间断性的去更新服务列表
- 选择可用的服务进行调用(交给IRule去实现,不同的轮询策略)
Ribbon 负载均衡器 LoadBalancer 源码解析
ZoneAwareLoadBalancer
默认的负载均衡器
@Override
public Server chooseServer(Object key) {
//如果
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
负载规则!!!
负载均衡器对server进行管理,而负载均衡规则就是对应的负载均衡算法(负载均衡器调用负载均衡规则),ribbon自带7种负载均衡规则实现:
com.netflix.loadbalancer.RoundRobinRule- 轮询com.netflix.loadbalancer.RandomRule- 随机com.netflix.loadbalancer.RetryRule- 重试,先按RoundRobinRule进行轮询,如果失败就在指定时间内进行重试com.netflix.loadbalancer.WeightedResponseTimeRule- 权重随机,响应速度越快,权重越大,越容易被选中。com.netflix.loadbalancer.BestAvailableRule- 先过滤掉不可用的处于断路器跳闸转态的服务,然后选择一个并发量最小的服务com.netflix.loadbalancer.AvailabilityFilteringRule-先过滤掉故障实例,再选择并发量较小的实例com.netflix.loadbalancer.ZoneAvoidanceRule- Spring-cloud-netfix-ribbon默认规则 复合判断server所在区域的性能和server的可用性进行服务的选择。
public interface IRule{
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
RoundRobinRule -轮询
RandomRule-随机
RetryRule-重试
指定子规则(默认轮询),使用子规则选择一个Server,然后不断重试,直到选择到一个Server是存活的或者超时中断。
在默认情况下
RetryRule=轮询,过滤掉不存活的server
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
//选择一个server
answer = subRule.choose(key);
//为null或者 不存或则
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
//中断任务
InterruptTask task = new InterruptTask(deadline
- System.currentTimeMillis());
//中断后 清除中断退出
while (!Thread.interrupted()) {
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
WeightedResponseTimeRule-响应时间作为权重
通过定时获取Server的响应时间作为权重加权随机。默认30s更新响应时间,
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
// get hold of the current reference in case it is changed from the other thread
List<Double> currentWeights = accumulatedWeights;
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int serverIndex = 0;
// last one in the list is the sum of all weights
double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);
// No server has been hit yet and total weight is not initialized
// fallback to use round robin 权重没有初始化则使用轮询
if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
server = super.choose(getLoadBalancer(), key);
if(server == null) {
return server;
}
} else {
// generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
double randomWeight = random.nextDouble() * maxTotalWeight;
// pick the server index based on the randomIndex
int n = 0;
for (Double d : currentWeights) {
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
server = allList.get(serverIndex);
}
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Next.
server = null;
}
return server;
}
初始化时默认30s间隔去更新响应时间权重。
//定时任务去计算权重
class DynamicServerWeightTask extends TimerTask {
public void run() {
ServerWeight serverWeight = new ServerWeight();
try {
serverWeight.maintainWeights();
} catch (Exception e) {
logger.error("Error running DynamicServerWeightTask for {}", name, e);
}
}
}
class ServerWeight {
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats == null) {
// no statistics, nothing to do
return;
}
double totalResponseTime = 0;
// find maximal 95% response time
for (Server server : nlb.getAllServers()) {
// this will automatically load the stats if not in cache
ServerStats ss = stats.getSingleServerStat(server);
totalResponseTime += ss.getResponseTimeAvg();
}
// weight for each server is (sum of responseTime of all servers - responseTime)
// so that the longer the response time, the less the weight and the less likely to be chosen
Double weightSoFar = 0.0;
// create new list and hot swap the reference
List<Double> finalWeights = new ArrayList<Double>();
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
double weight = totalResponseTime - ss.getResponseTimeAvg();
weightSoFar += weight;
finalWeights.add(weightSoFar);
}
setWeights(finalWeights);
} catch (Exception e) {
logger.error("Error calculating server weights", e);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
}
AvailabilityFilteringRule-过滤掉不可用进行轮询
过滤掉不可以用的,然后进行轮询。不可用的定义是处于熔断或者Server连接太多。详细看后面的过滤规则。
public AvailabilityFilteringRule() {
super();
predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, null))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
public Server choose(Object key) {
int count = 0;
Server server = roundRobinRule.choose(key);
while (count++ <= 10) {
//过滤 过滤规则是AvailabilityPredicate 过滤掉不健康的
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
return super.choose(key);
}
BestAvailableRule过滤掉熔断-并发量小的
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) {
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
//过滤掉熔断器
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
//选择连接数量最小的
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
ZoneAvoidanceRule-过滤后轮询
Spring-cloud-netfix-ribbon默认(全局)的负载均衡策略,在RibbonClientConfiguration中如果配置文件中没有对应负载均衡规则配置则创建一个**ZoneAvoidanceRule,其实它使用的负载均衡算法就是轮询**(记录nextIndex,然后CAS+1)。
@Bean
@ConditionalOnMissingBean
//全局配置
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
//默认实现
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
//过滤之后轮询
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//得到有资格的server
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
//incrementAndGetModulo 有资格的server进行轮询 即cas获取下一个下标值
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
//自增并取模 并cas替换直到成功即得到轮询后下标
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
将ZoneAvoidancePredicate和AvailabilityPredicate组合在一起作为过滤规则。即先过滤掉不可用区域,然后再过滤掉不可用的服务。
public ZoneAvoidanceRule() {
super();
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
过滤规则
ZoneAvoidancePredicate
ZoneAvoidancePredicate用于过滤掉不可用的区域。
public static Set<String> getAvailableZones(
Map<String, ZoneSnapshot> snapshot, double triggeringLoad,
double triggeringBlackoutPercentage) {
if (snapshot.isEmpty()) {
return null;
}
Set<String> availableZones = new HashSet<String>(snapshot.keySet());
/**如果只有一个zone 直接返回不需要判断*/
if (availableZones.size() == 1) {
return availableZones;
}
/**保存最坏的zone*/
Set<String> worstZones = new HashSet<String>();
/**所有的zone中平均负载最高值*/
double maxLoadPerServer = 0;
/**是否有一部分可用的负载 false时意为全部可用 true的时候有限可用*/
boolean limitedZoneAvailability = false;
for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
String zone = zoneEntry.getKey();
ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
int instanceCount = zoneSnapshot.getInstanceCount();
/**如果zone内没有可用实例 从可用的zone中移除 将部分可用赋值为true*/
if (instanceCount == 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
double loadPerServer = zoneSnapshot.getLoadPerServer();
/**如果熔断率大于传过来的阈值 或者实例的平均负载小于0 也从可用的zone中移除
* 前半部分条件:熔断实例数/总实例数 >= 阈值(阈值为0.99999d) 也就差不多是所有的server都被
* 熔断了 该zone才不可用
* 后半部分条件:loadPerServer < 0 这里是什么意思?什么情况下loadPerServer才会小于0 那就要
* 去看看 LoadBalancerStats#getZoneSnapshot()
* if (circuitBreakerTrippedCount == instanceCount)
* loadPerServer = -1
* 也就是所有的实例都熔断了 那么loadPerServer久没有任何意义了 所以就赋值为-1
* 那前半部分的条件和后半部分岂不是一样的?前半部分是可以配置的 通过下面
* ZoneAwareNIWSDiscoveryLoadBalancer.clientName.avoidZoneWithBlackoutPercetage 默认是0.99999d
* */
if (((double) zoneSnapshot.getCircuitTrippedCount())
/ instanceCount >= triggeringBlackoutPercentage
|| loadPerServer < 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
/**当前的平均负载和上一次最大的负载一样大 那就将当前的区域加入 最坏负载集合
* 这样worstZones 就会有多个值 一般情况下worstZones这个集合会出现一个值
* 但是如果有两个区域负载相同时 就会有2个值 以此类推
* */
if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
// they are the same considering double calculation
// round error
worstZones.add(zone);
} else if (loadPerServer > maxLoadPerServer) {
/**如果当前负载大于上一次的最大负载 就保存当前负载的区域 并且交换最大负载的值*/
maxLoadPerServer = loadPerServer;
worstZones.clear();
worstZones.add(zone);
}
}
}
}
/**如果全部区域都可用 并且最大负载没达到阈值 返回全部的可用区域
* triggeringLoad 负载阈值 默认值为0.2 通过
* ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold来设置
* 请求量/实例数 = 平均负载阈值 默认值为0.2有点不合理 加入一个区域10台服务器 有两个活跃的请求
* 这个zone的平均负载就是0.2。超过两个请求进来就会负载过重。
*
* 这么设置的后果:假如有两个zone区域 总会有一个被移除掉 会导致负载均衡策略完全失效。
* (对于这个结论我们可以在AvailableZonesTest中模拟)
*
* 所以生产环境如果有多个zone区域的话 建议不要使用默认值
* ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold = 50
* */
if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
// zone override is not needed here
return availableZones;
}
/**
* 这一步是吧最坏的负载区域移除掉 worstZones这个集合上面说了 一般会是一个值
* 当出现最大负载有多个区域时 才会有多个值
*
* 程序走到这里 availableZones肯定是不止一个zone的。因为在最开始 就进行了判断
* availableZones 如果仅有一个值 直接返回了。
* */
String zoneToAvoid = randomChooseZone(snapshot, worstZones);
if (zoneToAvoid != null) {
availableZones.remove(zoneToAvoid);
}
return availableZones;
}
-
如果
zone为null,那么也就是没有可用区域,直接返回null -
如果
zone的可用区域为1,也没有什么可以选择的,直接返回这一个 -
使用Set worstZones记录所有zone中比较状态不好的的zone列表,用maxLoadPerServer表示所有zone中负载最高的区域;用limitedZoneAvailability表示是否是部分zone可用(true:部分可用,false:全部可用),接着我们需要遍历所有的zone信息,逐一进行判断从而对有效zone的结果进行处理。
-
如果当前
zone的instanceCount为0,那就直接把这个区域移除就行,并且标记limitedZoneAvailability为部分可用,没什么好说的。 -
获取当前总的平均负载loadPerServer,如果zone内的
熔断实例数/总实例数 >= triggeringBlackoutPercentage或者loadPerServer < 0的话,说明当前区域有问题,直接执行remove移除当前zone,并且limitedZoneAvailability=true
- (
熔断实例数 / 总实例数 >= 阈值,标记为当前zone就不可用了(移除掉),这个很好理解。这个阈值为0.99999d也就说所有的Server实例被熔断了,该zone才算不可用了). loadPerServer = -1,也就说当所有实例都熔断了。这两个条件判断都差不多,都是判断这个区域的可用性。
- (
-
如果当前zone没有达到阈值,则判断区域的负载情况,从所有
zone中找到负载最高的区域(负载差值在0.000001d),则把这些区域加入到worstZones列表,也就是这个集合保存的是负载较高的区域。
-
-
通过上述遍历对区域数据进行计算后,最后要设置返回的有效区域数据。
- 最高负载
maxLoadPerServer仍旧小于提供的triggeringLoad阈值,并且并且limitedZoneAvailability=false(就是说所有zone都可用的情况下),那就返回所有的zone:availableZones。 (也就是所有区域的负载都在阈值范围内并且每个区域内的节点都还存活状态,就全部返回) - 否则,最大负载超过阈值或者某些区域存在部分不可用的节点时,就从这些负载较高的节点
worstZones中随机移除一个
- 最高负载
该方法后两个参数 triggeringLoad(负载阈值), triggeringBlackoutPercentage(熔断服务器的百分比) 默认值为0.2和0.9999 。负载阈值0.2 意味着 一个区域如果有10台服务器 有3个请求就会达到这个阈值,所以默认值设置的有点小,如果启用相关区域规则的组件 这个默认值是需要注意的地方。
即根据区域中整体的负载和熔断比例进行过滤
AvailabilityPredicate
当服务被熔断,或者获取请求数大于阈值则会被过滤掉。
@Override
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
private boolean shouldSkipServer(ServerStats stats) {
//熔断器已经打开或者活跃请求数大于限制
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
Ping 服务探测
IPing 接口负责向 Server 实例发送 ping 请求,判断 Server 是否有响应,以此来判断 Server 是否可用
接口只有一个方法 isAlive,通过实现类完成探测 ping 功能
public interface IPing {
public boolean isAlive(Server server);
}
void setupPingTask() {
if (canSkipPing()) {
return;
}
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
// 默认10s
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}
IPing 实现类如下:
- PingUrl:通过 ping 的方式,发起网络调用来判断 Server 是否可用(一般而言创建 PingUrl 需要指定路径,默认是 IP + Port)
- PingConstant:固定返回某服务是否可用,默认返回 True,表示可用
- NoOpPing:没有任何操作,直接返回 True,表示可用
- DummyPing:默认的类,直接返回 True,实现了 initWithNiwsConfig 方法
private static class SerialPingStrategy implements IPingStrategy {
@Override
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
//挨个进行ping
for (int i = 0; i < numCandidates; i++) {
results[i] = false; /* Default answer is DEAD. */
try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} catch (Exception e) {
logger.error("Exception while pinging Server: '{}'", servers[i], e);
}
}
return results;
}
}
和Nacos的整和
对应于每个服务都有以下配置-NacosRibbonClientConfiguration,即根据IClientConfig中ServiceID生成NacosServerList:通过NamingService获取对应的服务实例。@RibbonClients(defaultConfiguration = NacosRibbonClientConfiguration.class)为每个Ribbon子容器中增加一个NacosRibbonClientConfiguration。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnBean(SpringClientFactory.class)
@ConditionalOnRibbonNacos
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
//自动装配 且将NacosRibbonClientConfiguration作为默认配置 解析时+default
@RibbonClients(defaultConfiguration = NacosRibbonClientConfiguration.class)
public class RibbonNacosAutoConfiguration {
}
//----- 每个的默认配置 ------
@Configuration(proxyBeanMethods = false)
@ConditionalOnRibbonNacos
public class NacosRibbonClientConfiguration {
@Autowired
private PropertiesFactory propertiesFactory;
@Bean
@ConditionalOnMissingBean
//条件装配如果ServerListbean不存在,因为每个子容器中都有这一份配置
public ServerList<?> ribbonServerList(IClientConfig config,
NacosDiscoveryProperties nacosDiscoveryProperties,
NacosServiceManager nacosServiceManager) {
if (this.propertiesFactory.isSet(ServerList.class, config.getClientName())) {
ServerList serverList = this.propertiesFactory.get(ServerList.class, config,
config.getClientName());
return serverList;
}
NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties,
nacosServiceManager);
serverList.initWithNiwsConfig(config);
return serverList;
}
@Bean
@ConditionalOnMissingBean
public NacosServerIntrospector nacosServerIntrospector() {
return new NacosServerIntrospector();
}
}
NacosServerList
和Ribbon整合时一个ClientName对应一个NacosServerList,通过nacosServiceManager更新和获取服务。
nacos1.x版本基于http进行心跳,然后固定时间间隔拉取实例,当服务发生变化时Server会UDP通知一次,2.x则基于长链接发送心跳了。
public class NacosServerList extends AbstractServerList<NacosServer> {
//....
private String serviceId;
@Override
public List<NacosServer> getInitialListOfServers() {
return getServers();
}
@Override
public List<NacosServer> getUpdatedListOfServers() {
return getServers();
}
private List<NacosServer> getServers() {
try {
String group = discoveryProperties.getGroup();
//nameSe
List<Instance> instances = nacosServiceManager
.getNamingService(discoveryProperties.getNacosProperties())
.selectInstances(serviceId, group, true);
return instancesToServerList(instances);
}
catch (Exception e) {
throw new IllegalStateException(
"Can not get service instances from nacos, serviceId=" + serviceId,
e);
}
}
private List<NacosServer> instancesToServerList(List<Instance> instances) {
List<NacosServer> result = new ArrayList<>();
if (CollectionUtils.isEmpty(instances)) {
return result;
}
for (Instance instance : instances) {
result.add(new NacosServer(instance));
}
return result;
}
public String getServiceId() {
return serviceId;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
总结
1、Spring cloud openfeign它是一个基于feign的Http请求框架,它可以集成Sentinel或者hystrix做限流熔断也可以集成Ribbon(默认)loadbalancer做负载均衡。而feign向下屏蔽了http客户端的实现可自由选择客户端的实现,默认情况下使用JDK中HttpURLConnection。Openfeign提供了OkHttp和HttpClient的整合(只需引用依赖)。
2、Spring-cloud-netfix-ribbon默认负载均衡算法为ZoneAvoidanceRule,先过滤掉区域在过滤掉不可以用然后再轮询。
3、Spring cloud openfeign为每个服务(客户端)都创建一个Spring ApplicationContext()和当前应用ApplicationContext组成父子关系,配置都从其中获取。
4、实际上负载均衡也会和openfeign一样为每个客户端创建一个Spring ApplicationContext,设置parent为当前应用上下文。具体查看NamedContextFactory的子类
ps:上面的ApplicationContext为**AnnotationConfigApplicationContext**
问题
1、为什么需要对每个负载均衡客户端、feign客户端都创建一个ApplicationContext?
我想因该是为了配置隔离吧,父context共享公用的configuration,然后不同的子context有不同的configuration。因为feign中的各个配置其实都是交给Spring管理的,如果都在一个容器中创建,势必需要为不同feign的配置生成一个唯一id,还要维护配置和feignClient的对应关系,然后对复用也不够友好。在配置各种之后我们只需要通过对应注解来声明一个Client有哪些配置,不需要关心怎么Fegin是怎么组装这些配置的。
创建父子容器隔离配置后,多个client可以单独复用某一个配置 也可化整为零,只需要关注这个Client的即可