【Spring-cloud-OpenFegin源码解析】
文章目录
- OpenFegin介绍
- OpenFegin的特性
- OpenFegin的源码流程
- OpenFegin源码分析
OpenFegin介绍
Feign是一个声明式 Web 服务客户端。它简化了 Web 服务客户端的编写。要使用 Feign,只需创建一个接口并对其进行注解即可。它支持可插入式注解,包括 Feign 注解和 JAX-RS 注解。Feign 还支持可插入式编码器和解码器。Spring Cloud 增加了对 Spring MVC 注解的支持,并支持使用HttpMessage ConvertersSpring Web 中默认使用的注解。Spring Cloud 集成了 Eureka、Spring Cloud CircuitBreaker 以及 Spring Cloud LoadBalancer,以便在使用 Feign 时提供负载均衡的 http 客户端。
OpenFegin的特性
- 声明式调用 使用 Java 接口 + 注解定义远程调用
- 支持 Spring MVC 注解 @RequestMapping、@GetMapping 等
- 支持参数注解 @RequestParam、@PathVariable、@RequestHeader 等
- 支持负载均衡 内部可集成 Ribbon / Spring Cloud LoadBalancer
- 支持熔断降级 可集成 Sentinel / Resilience4j 等
- 支持拦截器 请求日志、Token 注入、自定义 Header
- 支持全局配置 自定义超时时间、编码器、解码器、日志等级等
OpenFegin的源码流程
添加依赖(Spring Boot 项目)
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用openFegin功能
@SpringBootApplication
@EnableFeignClients
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}
声明接口:
@FeignClient(name="storeClient")
public interface StoreClient {@RequestMapping(method = RequestMethod.GET, value = "/stores")List<Store> getStores();@RequestMapping(method = RequestMethod.GET, value = "/stores")Page<Store> getStores(Pageable pageable);@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")Store update(@PathVariable("storeId") Long storeId, Store store);@RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")void delete(@PathVariable Long storeId);
}
调用
class TestFegin{@Autowiredprivate StoreClient storeClient;@RequestMapping("/query")public List<Store> query(){return storeClient.getStore();}
}
配置
feign:client:config:default:connectTimeout: 5000readTimeout: 10000loggerLevel: FULLretryer:maxAttempts: 3httpclient:enabled: true
OpenFegin源码分析
源码下载:
1.下载版本Openfegin-2.1.3.RELEASE
2. 下载地址:https://github.com/spring-cloud/spring-cloud-openfeign/tree/2.1.x
源码分析:
分析源码入口:
开启Fegin的开关类,EnableFeginClient
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {/*** Alias for the {@link #basePackages()} attribute. Allows for more concise annotation* declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of* {@code @ComponentScan(basePackages="org.my.pkg")}.* @return the array of 'basePackages'.*/String[] value() default {};/*** Base packages to scan for annotated components.* <p>* {@link #value()} is an alias for (and mutually exclusive with) this attribute.* <p>* Use {@link #basePackageClasses()} for a type-safe alternative to String-based* package names.* @return the array of 'basePackages'.*/String[] basePackages() default {};/*** Type-safe alternative to {@link #basePackages()} for specifying the packages to* scan for annotated components. The package of each class specified will be scanned.* <p>* Consider creating a special no-op marker class or interface in each package that* serves no purpose other than being referenced by this attribute.* @return the array of 'basePackageClasses'.*/Class<?>[] basePackageClasses() default {};/*** A custom <code>@Configuration</code> for all feign clients. Can contain override* <code>@Bean</code> definition for the pieces that make up the client, for instance* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.** @see FeignClientsConfiguration for the defaults* @return list of default configurations*/Class<?>[] defaultConfiguration() default {};/*** List of classes annotated with @FeignClient. If not empty, disables classpath* scanning.* @return list of FeignClient classes*/Class<?>[] clients() default {};}
核心FeignClientsRegistrar,将当前的被FeginClient注解修饰的类,注册到BeanFactory中。
/** Copyright 2013-2019 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** https://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package org.springframework.cloud.openfeign;import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AbstractClassTestingTypeFilter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;/*** @author Spencer Gibb* @author Jakub Narloch* @author Venil Noronha* @author Gang Li*/
class FeignClientsRegistrarimplements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {// patterned after Spring Integration IntegrationComponentScanRegistrar// and RibbonClientsConfigurationRegistgrarprivate ResourceLoader resourceLoader;private Environment environment;FeignClientsRegistrar() {}static void validateFallback(final Class clazz) {Assert.isTrue(!clazz.isInterface(),"Fallback class must implement the interface annotated by @FeignClient");}static void validateFallbackFactory(final Class clazz) {Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances "+ "of fallback classes that implement the interface annotated by @FeignClient");}static String getName(String name) {if (!StringUtils.hasText(name)) {return "";}String host = null;try {String url;if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;}else {url = name;}host = new URI(url).getHost();}catch (URISyntaxException e) {}Assert.state(host != null, "Service id not legal hostname (" + name + ")");return name;}static String getUrl(String url) {if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) {if (!url.contains("://")) {url = "http://" + url;}try {new URL(url);}catch (MalformedURLException e) {throw new IllegalArgumentException(url + " is malformed", e);}}return url;}static String getPath(String path) {if (StringUtils.hasText(path)) {path = path.trim();if (!path.startsWith("/")) {path = "/" + path;}if (path.endsWith("/")) {path = path.substring(0, path.length() - 1);}}return path;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {registerDefaultConfiguration(metadata, registry);registerFeignClients(metadata, registry);}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"));}}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() {@Overrideprotected 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 interfaceAnnotatedBeanDefinition 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);}}}}private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {String className = annotationMetadata.getClassName();BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);validate(attributes);definition.addPropertyValue("url", getUrl(attributes));definition.addPropertyValue("path", getPath(attributes));String name = getName(attributes);definition.addPropertyValue("name", name);String contextId = getContextId(attributes);definition.addPropertyValue("contextId", contextId);definition.addPropertyValue("type", className);definition.addPropertyValue("decode404", attributes.get("decode404"));definition.addPropertyValue("fallback", attributes.get("fallback"));definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);String alias = contextId + "FeignClient";AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be// nullbeanDefinition.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);}private void validate(Map<String, Object> attributes) {AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);// This blows up if an aliased property is overspecified// FIXME annotation.getAliasedString("name", FeignClient.class, null);validateFallback(annotation.getClass("fallback"));validateFallbackFactory(annotation.getClass("fallbackFactory"));}/* for testing */ String getName(Map<String, Object> attributes) {String name = (String) attributes.get("serviceId");if (!StringUtils.hasText(name)) {name = (String) attributes.get("name");}if (!StringUtils.hasText(name)) {name = (String) attributes.get("value");}name = resolve(name);return getName(name);}private String getContextId(Map<String, Object> attributes) {String contextId = (String) attributes.get("contextId");if (!StringUtils.hasText(contextId)) {return getName(attributes);}contextId = resolve(contextId);return getName(contextId);}private String resolve(String value) {if (StringUtils.hasText(value)) {return this.environment.resolvePlaceholders(value);}return value;}private String getUrl(Map<String, Object> attributes) {String url = resolve((String) attributes.get("url"));return getUrl(url);}private String getPath(Map<String, Object> attributes) {String path = resolve((String) attributes.get("path"));return getPath(path);}protected ClassPathScanningCandidateComponentProvider getScanner() {return new ClassPathScanningCandidateComponentProvider(false, this.environment) {@Overrideprotected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {boolean isCandidate = false;if (beanDefinition.getMetadata().isIndependent()) {if (!beanDefinition.getMetadata().isAnnotation()) {isCandidate = true;}}return isCandidate;}};}protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());Set<String> basePackages = new HashSet<>();for (String pkg : (String[]) attributes.get("value")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg);}}for (String pkg : (String[]) attributes.get("basePackages")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg);}}for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {basePackages.add(ClassUtils.getPackageName(clazz));}if (basePackages.isEmpty()) {basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));}return basePackages;}private String getQualifier(Map<String, Object> client) {if (client == null) {return null;}String qualifier = (String) client.get("qualifier");if (StringUtils.hasText(qualifier)) {return qualifier;}return null;}private String getClientName(Map<String, Object> client) {if (client == null) {return null;}String value = (String) client.get("contextId");if (!StringUtils.hasText(value)) {value = (String) client.get("value");}if (!StringUtils.hasText(value)) {value = (String) client.get("name");}if (!StringUtils.hasText(value)) {value = (String) client.get("serviceId");}if (StringUtils.hasText(value)) {return value;}throw new IllegalStateException("Either 'name' or 'value' must be provided in @"+ FeignClient.class.getSimpleName());}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());}@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}/*** Helper class to create a {@link TypeFilter} that matches if all the delegates* match.** @author Oliver Gierke*/private static class AllTypeFilter implements TypeFilter {private final List<TypeFilter> delegates;/*** Creates a new {@link AllTypeFilter} to match if all the given delegates match.* @param delegates must not be {@literal null}.*/AllTypeFilter(List<TypeFilter> delegates) {Assert.notNull(delegates, "This argument is required, it must not be null");this.delegates = delegates;}@Overridepublic boolean match(MetadataReader metadataReader,MetadataReaderFactory metadataReaderFactory) throws IOException {for (TypeFilter filter : this.delegates) {if (!filter.match(metadataReader, metadataReaderFactory)) {return false;}}return true;}}}