在进行微服务之间的调用的时候,我们本质上都是通过http请求来进行的(参数处理,返回结果处理),在使用Feign之前我们都是使用的RestTemplate来完成这些工作的,类似于下面的这种方式:
UserInfo userInfo = this.restTemplate.getForObject("http://user-client/userInfo/getUser/{name}", UserInfo.class, name);
这仅仅只是一次接口的调用,往往微服务之间接口的调用存在于很多地方,如果每次接口调用都使用这种硬编码的方式看起来并不友好。那么是否存在一种SpringMVC的那种方式,通过类似于方法的调用来处理微服务之间接口的调用,这就是Feign。
Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。
首先引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
在主类上通过@EnableFeignClients
注解开启Spring Cloud Feign的支持功能。
定义FeignClient接口,@FeignClient注解指定服务名来绑定服务,然后再使用Spring MVC的注解来绑定具体该服务提供的REST接口。这里为了方便,创建了一个公共的Api,通过继承的方式来处理。
//公共api
public interface CommonApi {
/**
* 查找用户
* @param name
* @return
*/
@GetMapping("/userInfo/getUserByName/{name}")
Object findByName(@PathVariable("name") String name);
/**
* 增加用户
* @param userRequest
* @return
*/
@PostMapping("/userInfo/save")
Object saveUserInfo(@RequestBody UserRequest userRequest);
}
//定义FeignClient接口,并绑定服务提供者。fallbackFactory属性可以在服务不可用时自动调用fallback制定的处理方法
@FeignClient(name = "user-provider",fallbackFactory = UserFeignClientFallback.class)
public interface MovieService extends CommonApi {
}
//服务不可用进入回退逻辑
@Component
@Slf4j
public class UserFeignClientFallback implements FallbackFactory<MovieService> {
@Override
public MovieService create(Throwable throwable) {
return new MovieService() {
@Override
public Object findByName(String name) {
log.error("进入回退逻辑", throwable);
return new UserInfo(0L,"默认用户",0);
}
@Override
public Object saveUserInfo(UserRequest userRequest) {
log.error("进入回退逻辑", throwable);
return new UserInfo(0L,"默认用户",0);
}
};
}
}
属性说明
value、name:value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
contextId:如果有多个name值一样的服务,可以通过contextId来区分服务。也可以通过设置属性
spring.main.allow-bean-definition-overriding=true
url:用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。
path:指定FeignClient访问接口的统一前缀,和在类上使用@RequestMapping一样。
创建controller实现对Feign客户端的调用
@RestController
public class MovieController2 {
@Autowired
private MovieService movieService;
@GetMapping("/userInfo/getUserByName/{name}")
public Object findByName(@PathVariable("name") String name){
return movieService.findByName(name);
}
@PostMapping("/userInfo/save")
public Object findByNameAndAge(@RequestBody UserRequest userRequest){
return movieService.saveUserInfo(userRequest);
}
}
创建provider实现CommonApi
@RestController
public class UserService implements CommonApi {
@Autowired
private UserInfoRepository userInfoRepository;
@Override
public Object findByName(String name) {
UserInfo userInfo=userInfoRepository.findByName(name);
return userInfo;
}
@Override
public Object saveUserInfo(UserRequest userRequest) {
UserInfo userInfo=new UserInfo();
userInfo.setId(userRequest.getId());
userInfo.setAge(userRequest.getAge());
userInfo.setName(userRequest.getName());
return userInfoRepository.save(userInfo);
}
}
feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。
允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。
**如果@configuration注解那么所有的FeignClient都会使用这个配置类,Configuration属性是指定单独某个Feign。**例如有关于负载均衡规则、权限认证的一些配置
//首先通过configuration设置feign的配置类
@FeignClient(configuration = FeignAuthConfiguration.class)
public class FeignAuthConfiguration {
//可以在类中配置,也可以在配置文件中设置
/*
user-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
*/
@Bean
IRule getIRule(){
return new RoundRobinRule();
}
}
如果服务端开启类security权限认证,那么FeignClient也需要做相应的配置
首先服务端开启security权限认证
spring:
security:
user:
name: root
password: root
消费端设置FeignAuthConfiguration
public class FeignAuthConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("root", "root");
}
}
在feign上加配置
@FeignClient(name = "user-provider",configuration = FeignAuthConfiguration.class)
Feign在默认情况下使用的是JDK原生URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。我们可以用Apache的HTTP Client替换Feign原始的http client,从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud从Brixtion.SR5版本开始支持这种替换,首先在项目中声明ApcaheHTTP Client和feign-httpclient依赖,然后在application.properties中添加:
feign:
httpclient:
enabled: true
feign组件提供了请求操作接口RequestInterceptor,实现之后对apply函数进行重写就能对request进行修改,包括header和body操作。也可实现权限认证的操作
@Configuration
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//权限认证 cm9vdDpyb290 base64加密 原文:root:root
template.header("Authorization", "Basic cm9vdDpyb290");
//方法名
String method = template.method();
String url = template.url();
}
}
//如果为指定某个服务的拦截器在配置文件中配置
user-provider: 服务名
request-interceptors:
- com.example.userconsumer.config.MyBasicAuthRequestInterceptor
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需通过下面两个参数设置,就能开启请求与响应的压缩功能:
请求压缩一般用不上
feign:
compression:
request:
enabled: true
response: #设置返回值后,接受参数要改一下。
enabled: true
#也可以进行某种格式的压缩
feign:
compression:
request:
enabled: true
mime-types:
- text/xml
min-request-size: 2048
feign:
client:
config:
user-provider:
logger-level: basic
//上面有4种日志类型
none:不记录任何日志,默认值
basic:仅记录请求方法,url,响应状态码,执行时间。
headers:在basic基础上,记录header信息
full:记录请求和响应的header,body,元数据。
//上面的logger-level只对下面的 debug级别日志做出响应。
logging:
level:
com.example.consumermovie.service.MovieService: debug
先从启动类的@EnableFeignClients开始说:首先里面的属性默认全是空的
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
...
}
这里使用了@Import注解来导入类注册成bean,通过import注解来注册bean有几种方式:
实现ImportSelector接口,spring容器就会实例化类,并且调用其selectImports方法;
实现ImportBeanDefinitionRegistrar接口,spring容器就会调用其registerBeanDefinitions方法;
带有Configuration注解的配置类。
这里是第二种方式
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
/**
注册@EnableFeignClients提供的自定义配置类中的相关bean。
此配置类是被@Configuration注解修饰的配置类,
它会提供一系列组装FeignClient的各类组件实例,比如Decoder、Encoder等。
*/
registerDefaultConfiguration(metadata, registry);
//注册@FeignClient注解对应的属性,并注册相应的bean
registerFeignClients(metadata, registry);
}
}
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//首先获取到@EnableFeignClients的属性键值对Map集合(value、basePackages、defaultConfiguration...)
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();
}
//注册自定义配置类,如果在@EnableFeignClients没有指定的话就是默认的
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
//注册
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
//通过BeanDefinitionBuilder建造BeanDefinition对象
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
//注册bean
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
这里注册bean的时候名称是主类的“全限定名+FeignClientSpecification”,这里的FeignClientSpecification其实就是Feign组件环境,保存了名称和配置类的信息。
class FeignClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
FeignClientSpecification() {
}
FeignClientSpecification(String name, Class<?>[] configuration) {
this.name = name;
this.configuration = configuration;
}
}
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 获取ClassPath扫描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
// 获取ClassPath扫描器
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
//获取EnableFeignClients中所有的属性信息
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
//注解类型过滤器, 只过滤FeignClient
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
//获取所有的clients注解的值,并强转成Class数组
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
//程序刚启动的时候attrs.get("clients")默认是空的,因此clients.length==0
if (clients == null || clients.length == 0) {
// 扫描器设置过滤器
scanner.addIncludeFilter(annotationTypeFilter);
//获取需要扫描的基础包集合 (主类的包名)
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
//前面scanner设置了注解类型过滤器,因此这个candidateComponents的set集合是所有@FeignClient注解类的集合
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
//验证BeanDefinition是否是接口
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//获取FeignClient注解的属性
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
//获取FeignClient的服务名称(根据contextId、name、value、serviceId属性获取)
String name = getClientName(attributes);
//注册FeignClient配置类的Bean
registerClientConfiguration(registry, name,
attributes.get("configuration"));
//注册FeignClient的Bean
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
//获取BeanDefinition的全限定名
String className = annotationMetadata.getClassName();
// 2.BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
// AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder
// 然后注册到Spring中
// 注意:beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是
// FeignClientFactoryBean类
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
// 3.添加FeignClientFactoryBean的属性,
// 这些属性也都是我们在@FeignClient中定义的属性
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);
//设置别名 可以看到是通过contextId来设置的
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;
}
// 5.定义BeanDefinitionHolder
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
//这里BeanDefinition的name为FeignClient注解类的全限定名,类为FeignClientFactoryBean
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
在这里做了几件事情:
将EnableFeignClients注解对应的配置属性注入;
将FeignClient注解对应的属性注入。
生成FeignClient对应的bean,注入到Spring 的IOC容器。
在registerFeignClient
方法中构造了一个BeanDefinitionBuilder
对象,BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition,AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder 然后注册到Spring中。
beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是FeignClientFactoryBean类。
通过上面的分析得出所有FeignClient注解修饰的Bean实际类型是FeignClientFactoryBean。
FeignClientFactoryBean
作为一个实现了FactoryBean
的工厂类,那么每次在Spring Context 创建实体类的时候会调用它的getObject()
方法。
@Override
public Object getObject() throws Exception {
return getTarget();
}
/**
* @param the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
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);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(type, name, url));
}
这里首先需要从applicationContext中获取FeignContext对象,这个对象什么时候被注入到Bean中的呢?点击这个类查看被引用的地方,发现在FeignAutoConfiguration
声明了这个Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
FeignHttpClientProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public HasFeatures feignFeature() {
return HasFeatures.namedFeature("Feign", Feign.class);
}
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
...
}
这里会把List
set进去,还记得在5.1.2中registerClientConfiguration
方法
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
//通过BeanDefinitionBuilder建造BeanDefinition对象
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
//注册bean
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
在注册BeanDefinition的时候, configuration 其实也被作为参数,传给了 FeignClientSpecification。 所以这时候在FeignContext中是带着configuration配置信息的。
接着通过context对象获取builder对象
Feign.Builder builder = feign(context);
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(type);
// @formatter:off
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的调用分析