使用Feign实现声明式Rest调用
文章目录
1.什么是Feign
Feign是一个http请求调用的轻量级框架。使用Feign,可以直接以Java接口注解的方式发送Http请求,而不需要在Java中通过封装HTTP工具类来发送请求。
Feign源码地址:Feign
2.Feign解决了什么问题
Feign封装了Http 请求调用流程,实现了申明式Http接口调用。
使用Feign的方式调用远程服务,服务消费者与生产者不需要实现同一个接口,可以做到消费者与生产者代码上的完全解耦。就代码耦合而言,Feign与jdk提供的rmi和阿里的dubbo有所差异,后者进行远程服务调用需要实现共同的api。另外服务消费者一方只需要关注FeignClient配置即可,不需要关注具体Http Request的实现,所以说Feign最终目的是将Java Http客户端调用过程变得简单。
从角色职能划分,Feign提供http调用服务流程如下:
3.Feign工作原理
Feign是一个伪java客户端,Feign不做任何的请求处理。Feign通过处理注解生成Request模板,从而简化了Http API的开发,开发人员可以使用注解的方式定制Request API模板。在发送Http Request请求之前,Feign通过处理注解的方式替换掉Request模板中的参数,生成真正的Request,并交给Java Http客户端处理。
综合来讲,发送一个Http请求,Feign做了两件事情:
1、Http请求处理流程封装,包含:请求行、请求头、请求体、响应;
2、选择Http Client发送请求。
我们在通过源码去理解Feign原理的时候,不妨带着这两个问题,从源码中理解,Feign是如何处理这两个问题的,这样对于我们理解Feign会有所帮助。
3.1.流程梳理
我们可以把Feign处理Http请求的基本流程分为两个部分,第一部分是初始化阶段,进行Proxy和MethodHandler的创建,第二部分则是具体请求处理流程。
3.1.1.初始化流程
初始化流程基本如下:
Feign初始化过程基本分为两个部分:
1.ReflectiveFeign根据指定的Contract为每一个方法创建了一个SynchronousMethodHandler;
2.基于动态代理,为Target接口创建了一个proxy对象,同时定义一个统一的InvocationHandler用于请求处理,将请求分发到指定的SynchronousMethodHandler处理。
3.1.2.Request处理过程
Request处理过程基本如下:
Feign封装了整个Request的处理过程,按照请求顺序,如下:
1.具体方法处理类SynchronousMethodHandler创建请求模板;
2.对Request请求进行预处理,编码;
3.将Request交给client去执行处理,若有拦截器先执行拦截器;
4.返回结果处理,解码;
5.返回结果最终转化为javaBean交付给具体方法处理类SynchronousMethodHandler。
3.2.FeignClient注册
在@EnableFeignClients标签中,import了FeignClientsRegistrar,通过FeignClientsRegistrar的registerBeanDefinitions方法完成了FeignClient的Bean的注入。程序启动时,会检查是否有@EnableFeignClients注解,如果有,则会执行FeignClientsRegistrar的registerBeanDefinitions方法。其中registerBeanDefinitions代码如下:
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//扫描EnableFeignClients标签里配置的信息,注册到beanDefinitionNames中。
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
其中registerFeignClients完成了对FeignClient的注册,代码如下:
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());
// 定义Filter规则,用于scanner过滤
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 获取EnableFeignClients注解中的clients属性的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
// @EnableFeignClients提供了clients属性用于指定扫描的clients
// 存在指定的clients,依次将client所在的package添加到basePackages
...// 省略代码
}
for (String basePackage : basePackages) {
// 从basePackage中扫描到的FeignClient
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
// 注意,@FeignClient只能标注在接口上
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);
/**
* 关键地方:Feign子容器概念:
* 在注入FeignAutoConfiguration类的时候,注入了一个FeignContext对象,这个就是Feign的子容器。
* 这里面装了List<FeignClientSpecification>对象,FeignClientSpecification对象的实质就是在@feignClient上配置的configuration指定对象的值
* 这个地方比较关键,主要是因为后期对feign客户端的编码解码会用到自定义的类
*/
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注册feignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
大致逻辑如下:
1.获取EnableFeignClients注解的相关属性;
2.定义按照FeignClient注解过滤的过滤器annotationTypeFilter;
3.根据注解和定义的过滤规则确定扫描范围basePackages,basePackages默认是启动类的同级目录,若EnableFeignClients指定了clients,则basePackages是clients指定的每一个类的同级目录的集合;
4.扫描basePackages中FeignClient,依次注入到Spring容器中。
从上文中,我们可以了解到在registerBeanDefinitions是方法中完成了FeignClient的Bean注入,那么registerBeanDefinitions这个方法又是在上面时候执行的呢?我们不妨进一步探索一下,跟着Spring的源码走下去,看过源码的人都会直接看到AbstractApplicationContext#refresh()方法,整体整理一下代码:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// 扫描本项目里面的java文件,把bean对象封装成BeanDefinitiaon对象,
//然后调用DefaultListableBeanFactory#registerBeanDefinition()方法把beanName放到DefaultListableBeanFactory 的 List<String> beanDefinitionNames 中去
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
postProcessBeanFactory(beanFactory);
// 在这里调用到FeignClientsRegistrar对象的registerBeanDefinitions()方法
invokeBeanFactoryPostProcessors(beanFactory);
//从DefaultListableBeanFactory里面的beanDefinitionNames中找到所有实现了BeanPostProcessor接口的方法,如果有排序进行排序后放到list中
registerBeanPostProcessors(beanFactory);
//Spring的国际化
initMessageSource();
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
registerListeners();
// Spring的IOC、ID处理。Spring的AOP。事务都是在IOC完成之后调用了BeanPostProcessor#postProcessBeforeInitialization()和postProcessBeforeInitialization()方法,AOP(事务)就是在这里处理的
finishBeanFactoryInitialization(beanFactory);
// 执行完之后调用实现了所有LifecycleProcessor接口的类的onRefresh()方法,同时调用所有观察了ApplicationEvent接口的事件(观察者模式)
finishRefresh();
}
catch (BeansException ex) {
// 找到所有实现了DisposableBean接口的方法,调用了destroy()方法,这就是bean的销毁
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
throw ex;
}
finally {
resetCommonCaches();
}
}
}
根据上面整理的代码发现,FeignClientsRegistrar#registerBeanDefinitions()方法是在扫描完bean之后,只放了一个beanname的情况下, 并没有进行IOC注册的时候调用的,这就是Spring动态扩展Bean。另外,实现BeanDefinitionRegistryPostProcessor接口的所有方法也会在这里调用下postProcessBeanDefinitionRegistry()方法。
总结一下:
我们平时工作和学习中,留心的话,不难发现:spring作为整合专家,在整合其它框架时存在一个基本套路:1.自定义三方注解、2.定义注册器Registrar,扫描注解标准类注入到spring的IoC容器中。Feign正是其中之一,对这方面比较感兴趣的话,不妨去深入研究一下spring自定义注解和spring-bean。
3.3.创建代理
注入BeanDefinition之后, ReflectiveFeign内部使用了jdk的动态代理为目标接口生成了一个代理类,这里会生成一个InvocationHandler统一的方法处理器,同时为接口的每个方法生成一个SynchronousMethodHandler拦截。
下面围绕两个方面讲述:
1、如何创建代理,创建的是谁的代理;
2、请求是怎么分发到具体的SynchronousMethodHandler方法处理器的。
ReflectiveFeign#newInstance代码如下:
public <T> T newInstance(Target<T> target) {
// 为每一个method创建一个MethodHandler
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
// method 容器,key为targetd的Method
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
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,总创建InvocationHandler,最终会分到具体的SynchronousMethodHandler
InvocationHandler handler = factory.create(target, methodToHandler);
// 创建代理对象,经过源码追踪可以了解到target即为@FeignClient注解标注的接口
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
上面代码中targetToHandlersByName.apply(target),根据contract协议为每一个method创建了一个MethodHandler,具体实现类是SynchronousMethodHandler,代码如下:
public Map<String, MethodHandler> apply(Target key) {
List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
···// 省略代码
result.put(md.configKey(),
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
}
return result;
}
进入factory.create可以发现,实际上创建的MethodHandler即为SynchronousMethodHandler,代码如下:
public MethodHandler create(Target<?> target, MethodMetadata md,
RequestTemplate.Factory buildTemplateFromArgs,
Options options, Decoder decoder, ErrorDecoder errorDecoder) {
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
logLevel, md, buildTemplateFromArgs, options, decoder,
errorDecoder, decode404);
}
另外,在创建InvocationHandler的时候,我们发现传入的参数是methodToHandler,从上文中可知,其中key为method,value为SynchronousMethodHandler对象。继续代码跟进,可以发现创建的InvocationHandler实际上就是FeignInvocationHandler,并且将methodToHandler赋值给了dispatch,代码如下:
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
}
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
总结一下:
1.ReflectiveFeign内部使用了jdk的动态代理为目标接口(@FeignClient注解标注的接口)生成了一个代理类,并且生成统一的InvocationHandler;
2.为每一个Method创建SynchronousMethodHandler,并将方法及放入dispatch方法容器中,其中key为Method,保证key的唯一性,value为具体的SynchronousMethodHandler。那么在后面的接口调用中,则可以通过具体的Method获取到具体的SynchronousMethodHandler了。
3.4.接口调用
根据上文创建代理部分可知,当调用Feign Client接口里面的方法时,该方法会被FeignInvocationHandler拦截,并且调用invoke方法,在invoke方法中,完成了分发到指定的SynchronousMethodHandler处理的动作,代码如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
···// 省略代码
return dispatch.get(method).invoke(args);
}
SynchronousMethodHandler处理请求时,根据传入参数生成RequestTemplate对象,该对象即为请求模板,代码如下:
@Override
public Object invoke(Object[] argv) throws Throwable {
// 根据Target接口中的方法注解,创建请求模板
RequestTemplate template = buildTemplateFromArgs.create(argv);
// 获取重试策略,默认不重试
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
// 请求异常重试
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
在 executeAndDecode()方法中,通过RequestTemplate创建Request请求对象,然后用Http Client执行request,即通过Http Client进行Http请求获取结果,代码如下:
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
···// 省略代码,参数编码
// client发送request请求
response = client.execute(request, options);
···// 省略代码,response解码
}
此处,进入 targetRequest()方法,发现执行了一些列的拦截器,代码如下:
Request targetRequest(RequestTemplate template) {
// 执行拦截器
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
// 生成Request并返回
return target.apply(new RequestTemplate(template));
}
总结一下调用步骤:
1.以method为key,获取到具体的SynchronousMethodHandler;
2.创建请求模板;
3.获取重试策略;
4.执行拦截器;
5.创建Request;
6.参数编码;
7.发送请求;
8.response解码;
9.请求异常,执行重试策略。
3.5.重试策略
从SynchronousMethodHandler的invoke方法中可以看到,声明了一个重试器Retryer,在请求执行失败后会根据重试策略进行请求重试,调用Retryer的continueOrPropagate方法。从FeignClientsConfiguration代码中可以看到默认定义的Retryer是不进行重试的,因为continueOrPropagate方法直接抛出了异常,代码如下:
@Configuration
public class FeignClientsConfiguration {
···// 省略代码
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
···// 省略代码
}
public interface Retryer extends Cloneable {
···// 省略代码
Retryer NEVER_RETRY = new Retryer() {
// 重试方法,直接返回异常,不重试
@Override
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
···// 省略代码
}
所以说,如果需要具有重试功能,可以重新定义一个Retryer覆盖默认的即可,Feign也默认提供Retryer.Default的重试策略,可以定义好重试参数后直接使用,不需要拓展重试策略了。
注意:
spring-cloud-feign之所以默认Retryer.NEVER_RETRY,即不重试,是因为spring-cloud-feign整合了ribbon,ribbon也有重试策略,如果fegin也开启重试策略,容易造成混乱。如果feign单独使用的情况下,建议定义一下重试策略。
3.6.Client动态注入
看到这里,其实还有一个疑问,执行Http请求使用的Client是什么时候初始化的,整体一下,提供两个点思考方向:
1.发送http请求的client工具类是怎么集成进去的;
2.Feign是怎么实现负载均衡的。
先看第1个问题:
Feign默认集成了3种Http调用工具,分布为:ApacheHttpClient、OkHttpClient、HttpURLConnection。默认情况下使用的是HttpURLConnection,当引入ApacheHttpClient依赖时,client即为ApacheHttpClient,想切换为OkHttpClient,只需要将依赖替换为OkHttpClient即可。相关加载原理可以查看FeignAutoConfiguration类中对于ApacheHttpClient和OkHttpClient的加载条件。
对于性能有要求的项目中,建议不要使用HttpURLConnection,可以使用OkHttpClient或者ApacheHttpClient,对这3个工具类性能有兴趣的同学,可以深入了解一下,做一下对比。
接下来说一下第2个问题:
用过Feign的同学可能知道,spring-cloud-feign是支持负载均衡的,而第1个问题中提到的3种http工具类本身是不支持负载均衡的。那么,Feign是怎么保证初始化在内存中的client能够进行负载均衡的呢?这里的client有两个实现类,分别是Client.Defaut和LoadBalancerFeignClient,而默认值是Client.Defaut。继续阅读源码,发现FeignClientFactoryBean中的loadBalance方法会重置client,程序启动时,从这里使用LoadBalancerFeignClient的实例覆盖了默认的Client.Defaut,代码如下:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,ApplicationContextAware {
···// 省略代码
@Override
public Object getObject() throws Exception {
···// 省略代码
if (!StringUtils.hasText(this.url)) {
···// 省略代码
return loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
···// 省略代码
}
···// 省略代码
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
// 重置client,设置为loadBalanceClient
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
···// 省略代码
}
···// 省略代码
}
总结一下:
Client的注册使用了动态注入的方式,其实现逻辑是根据FeignClient是否配置了指定的url,如果没有配置url则使用负载均衡策略,配置了url,则直接使用url绑定的服务。我们平时编码直接使用@Service注解,而这种方式是静态注入。
4.Feign使用示例
4.1.原生Feign使用
4.1.1.需求
- 用户下单;
- 通过资金账号查询资产;
- 下单时对用户等级做身份认证,给高等级的机构用户提供快速下单渠道。
4.1.2.创建一个服务端
新建一个Spring Boot的Moudle工程,命名traderServer-1,满足需求中的相关接口,controller中代码如下:
@RestController
@RequestMapping("/trade")
public class TradeController {
@RequestMapping(value="/queryFund")
public String queryFund(String account) {
return "tradeServer-1,账户余额:1,000,000";
}
@RequestMapping(value="/order")
@ResponseBody
public String orderJSON(String stock, Double price, Double count) {
return "tradeServer-1,下单成功";
}
@RequestMapping(value="/orderJSON")
@ResponseBody
public String orderJSON(@RequestBody JSONObject body) {
return "tradeServer-1,下单成功";
}
}
4.1.3.新建原生Feign客户端
public interface ITradeService {
@RequestLine("GET /trade/queryFund?account={account}")
String queryFund(@Param("account") String account);
@RequestLine("POST /trade/order")
@Headers("Content-Type: application/json")
@Body("%7B\"stock\": \"{stock}\", \"price\": {price}, \"count\":{count}%7D")
String order(@Param("stock") String stock, @Param("price") Double price, @Param("count") Double count);
}
public class TestFeign {
public static void main(String[] args) {
ITradeService tradeService = Feign.builder()
.options(new Options(2000, 6000))
.target(ITradeService.class, "http://localhost:2002");
String result = tradeService.queryFund("xumiao");
System.out.println(result);
result = tradeService.order("300033", 20.0, 1000.0);
System.out.println(result);
}
}
从上文中可以看到,Feign支持get和post请求,并且新定义了一套注解,这种方式一定程度上提高了学习成本。Spring对feign进行整合后,对Spring MVC注解做了一定程度上的支持,基本满足项目中的使用,推荐使用Spring MVC注解。Feign默认的协议规范,如下:
4.2.Spring-cloud-feign用法
4.2.1.新建一个Feign客户端
新建一个Spring Boot的Moudle工程,命名spring-cloud-feign,在pom文件中加入相关依赖,application.yml文件中增加eureka相关配置启动类增加@EnableFeignClients注解,开启Feign Client功能,该程序就具备了Feign功能了。
根据需求,只需要创建一个包含资金查询和交易下单的接口即可,在接口上加@FeignClient注解来声明一个Feign Client,其中name为远程调用其他服务的服务名,本工程中使用eureka作为注册中心,则name的值即为服务端注册在eureka中的服务名称,代码如下:
@FeignClient(name = "trade-server")
public interface ITradeService {
@RequestMapping(value = "/trade/queryFund")
String queryFund(@RequestParam("account") String account);
@RequestMapping(value = "/trade/order")
String order(@RequestParam("stock") String stock, @RequestParam("price") Double price,
@RequestParam("count") Double count);
@RequestMapping(value = "/trade/orderJSON")
String orderJson(@RequestBody JSONObject json);
}
新增相关controller,提供外部调用接口,使用ITradeService进行相关远程服务的调用,部分代码如下:
@RestController
@RequestMapping("/trade")
public class TradeController {
@Autowired
private ITradeService tradeService;
@RequestMapping(value="/queryFund")
public String queryFund(String account) {
return tradeService.queryFund(account);
}
...
}
至此,已经完成需求中的下单和资金查询的功能,至于普通用户和机构客户的身份认证可以通过拦截器来实现,下文将在讲述拦截器的时候进一步实现该功能。浏览器中访问http://localhost:2000/trade/queryFund?account=3302…即可访问提供的服务了。结果如下:
复制traderServer-1命名为traderServer-2,同时启动traderServer-1和traderServer-2,发现Feign具备负载均衡功能。因为Feign本身并不支持负载均衡,属于Ribbon中的内容,有兴趣的同学建议去了解一下Ribbon。至此,工程架构图如下:
5.FeignClient配置
5.1.常规配置
FeignClient默认的配置类为FeignClientsConfiguration,打开这个类,可以发现这个类注入了很多Feign相关的配置Bean,包括Retryer、FeignLoggerFactory、FormattingConversionService等。另外,Decoder、Encoder和Contract这3个类使用@ConditionalOnMissingBean标记,即在没有Bean注入的情况下,会自动注入默认配置的Bean,部分代码如下:
@Configuration
public class FeignClientsConfiguration {
···//省略代码
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
···//省略代码
}
我们在实际使用中,可根据具体需求覆盖掉 FeignClientsConfiguration类中默认的配置Bean,从而达到自定义配置的目的。例如在Feign默认的配置在请求失败后,重试次数为0。现在希望请求失败后能够重试,这时写一个配置FeignConfig类,在该类中注入Retryer的Bean,覆盖掉默认的Retryer的Bean,并将FeignConfig指定为ITradeService的配置类。
FeignConfig类代码如下:
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}
注意:@FeignClient标注的目标接口类中使用的方法注解一定要与Contract契约相匹配。
我们可以写个例子看一下,不匹配的时候会有什么现象,看下面的例子:
此时,@FeignClient标注接口方法中使用的还是MVC契约的注解,当用Feign原生契约覆盖默认的MVC契约时,在原工厂中新建一个FeignConfiguration 配置类,代码如下:
@Configuration
public class FeignConfiguration
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}
在@Configuration不被注释并且配置类与启动类在统计目录下时,启动服务,会报一个比较常见的错误,如下:
报错原因:
当把Contract替换为feign.Contract.Default()后,ITradeService中方法上使用的注解还是基于MVC的spring-web包中的注解,两种契约出现冲突,所以抛出此异常。同理,当使用MVC契约,接口中使用Feign原生注解时,也会抛出此异常。
除上述情况之外,在不加@Configuration时,虽然不会启动报错,但是一旦 FeignConfiguration被@FeignClient使用,并且接口被类似@Autowired注解标记,启动会报同样的错,代码如下:
@FeignClient(name = "trade-server", configuration = FeignConfiguration.class)
public interface TradeFeignClient {
···// 省略代码
}
@RestController
@RequestMapping("/trade")
public class TradeController {
@Autowired
private TradeFeignClient tradeFeignClient;
···// 省略代码
}
若将改配置类置于@ComponentScan扫描范围(默认启动类同级目录)之外,此时,可启动正常。
根据这个现象,可以得出一个结论:
加了@Configuration注解,那么该类不能存放在主应用程序上下文@ComponentScan所扫描的包中。否则, 该类中的配置的feign.Decoder、feign.Encoder、feign.Contract 等配置就会被所有的@FeignClient共享,一旦Contract契约与注解不匹配时,会出错,所以最好不要混用。
5.2.拦截器配置
Client在执行Http Request之前,会执行相关RequestInterceptor拦截器,而Feign中默认也实现了BasicAuthRequestInterceptor,用于访问服务时,进行用户名和密码的基础认证,一般与Spring-cloud-security共同使用。同样,可通过配置类进行拦截器的定义,代码如下:
public class FooConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestlnterceptor() {
return new BasicAuthRequestInterceptor("organ", "123456");
}
}
增加上述代码后,引入上述FooConfiguration的FeignClient就具有HttpBasic认证的功能了。
我们再回顾上文中第3个需求,对机构和普通用户做一个身份认证,以便给高等级机构用户提供一个快速下单通道。对于此处的用户身份认证,可以采用spring-cloud-security做基础认证。
具体方案如下:
首先复制一下trade-server工程,命名为trade-server-auth,引入spring-cloud-security相关依赖,增加security相关配置类,代码如下
@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别保护
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* @param authenticationManagerBuilder
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有的请求,都需要经过HTTP basici人证
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(
// 机构角色
manager.createUser(User.withUsername("organ").password("123456").roles("ORGAN").build());
// 个人角色
manager.createUser(User.withUsername("person").password("123456").roles("PERSON").build());
return manager;
}
}
上述代码中,约定了organ和person这两种用户可以访问下单服务,所有请求都需要做HttpBasic认证。
服务提供方已经做好相关security的基础认证,服务调用方在调用服务的时候将身份信息传递过来即可,服务提供者根据调用者传入身份信息进行身份认证,机构用户则可以走机构用户下单的快速渠道。
接下来编写客户端相关代码,复制spring-cloud-feign命名为spring-cloud-feign-auth,新建TradeFooClient类引入上文中的FooConfiguration配置,代码如下:
@FeignClient(name = "trade-server-auth", configuration = FooConfiguration.class)
public interface TradeFooClient {
@RequestMapping(value = "/trade/queryFund")
String queryFund(@RequestParam("account") String account);
@RequestMapping(value = "/trade/orderJson")
String orderJson(@RequestParam(value = "stock") String stock, @RequestParam(value = "price") Double price,
@RequestParam(value = "count") Double count);
}
此时,我们已经基于拦截器的方式,实现了对用户身份识别,至于上文中第3个需求,机构用户使用快色渠道下单,只需要在识别身份后做对应的分发即可。
6.Feign之负载均衡
Fegin本身不支持负载均衡,其整合了Ribbon,通过Ribbon实现负载均衡。
FeginRibbonClientAutoConfiguration类通过@Import引入了HttpClientFeignLoadBalancedConfiguration、Ok-HttpFeignLoadBalancedConfiguration、DefaultFeignLoadBalancedConfiguration,不同版本可能有差异,但是目的都是为了配置Client的类型,分别为ApacheHttpClient、OkHttp和HttpURLConnection。3个配置类最终向容器注入的都是Client的实现类LoadBalancerFeignClient,即负载均衡客户端。查看LoadBalancerFeignClient的execute方法,代码如下:
@Override
public Response execute(Request request, Request.Options options) throws IOException {
···// 省略代码
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
···// 省略代码
}
其中 executeWithLoadBalancer()方法,即通过负载均衡的方式来执行网络请求。代码继续跟进到LoadBalancerCommand,其中selectServer()方法则为选择服务进行负载均衡的方法,代码如下:
private Observable<Server> selectServer() {
return Observable.create(new OnSubscribe<Server>() {
@Override
public void call(Subscriber<? super Server> next) {
try {
Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
next.onNext(server);
next.onCompleted();
} catch (Exception e) {
next.onError(e);
}
}
});
}
由上述代码可知,负载均衡的服务选择策略是 loadBalancerContext实现的,是ribbonloadbalancer包中的类。实际上feign本身是没有负载均衡能力的,spring-cloud-feign整合了ribbon使其具有负载均衡功能。如果需要有效的使用feign的负载均衡功能,建议先熟悉一下ribbon负载均衡的用法。
同时启动两个server时,工程架构图,如下:
7.总结
总的来说,Feign的源码实现过程如下:
1.首先通过@EnableFeignClients注解开启FeignClient功能,只有这个注解存在,才会在程序启动时开启对@FeignClient注解包的扫描。
2.根据Feign的规则实现接口、并在接口上面加上@FeignClient注解。
3.程序启动后,会进行包扫描,扫描所有的@FeignClient的注解类,并将这些信息注入IoC容器。
4.当接口的方法被调用时,通过JDK的代理类生产具体的RequestTemplate模板对象。
5.根据RequestTemplate再生成Http请求的Request对象。
6.Request对象交给Client处理,其中Client的网络请求框架可以是HttpURLConnection、HttpClient和OkHttp。
7.最后Client被封装到LoadBalanceClient类,这个类结合Ribbon做到了负载均衡。
8.参考文献
1.《深入理解Srping Cloud 与微服务构建》
2.https://blog.csdn.net/luanlouis/article/details/82821294