Java项目中接口调用:
上面是常用的方法,我们下面介绍一个简单的Feign。Feign是一个声明式的REST的客户端,能让REST调用更加简单。Feign提供HTTP模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数,格式,地址等信息。Feign完全代理http请求,只需要像调用方法一样调用就可以完成服务请求。
Spring Cloud对Feign进行了封装,使其支持SpringMVC注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合用于支持负载均衡。
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
1.增加依赖:
org.springframework.cloud
spring-cloud-starter-openfeign
2.定义一个Feign的客户端,以接口形式存在:
// 使用 @FeignClient 注解来指定提供者的名字
@FeignClient(value = "eureka-client-provider")
public interface TestClient {
// 这里一定要注意需要使用的是提供者那端的请求相对路径,这里就相当于映射了
@RequestMapping(value = "/provider/xxx",
method = RequestMethod.POST)
CommonResponse> getPlans(@RequestBody planGetRequest request);
}
3.在Controller层中像调用Service层一样调用(想要共用,可以单独创建一个API Client的公共项目,基于约定的形式,每写一个接口就写一个调用的Client,后面打成共用的Jar,无论哪个项目想使用,只需要调用公共的SDK jar,就可以了)
@RestController
public class TestController {
// 这里就相当于原来自动注入的 Service
@Autowired
private TestClient testClient;
// controller 调用 service 层代码
@RequestMapping(value = "/test", method = RequestMethod.POST)
public CommonResponse> get(@RequestBody planGetRequest request) {
return testClient.getPlans(request);
}
}
4.在启动类上加上@EnableFeignClients注解,如果Feign定义的接口和启动类不在一个包名下,,还需要定制扫描的包名@EnableFeignClients(basePackages="")
查看FeiginClient注解的源码:
@Target({ElementType.TYPE}) //表明注解使用在接口上
@Retention(RetentionPolicy.RUNTIME) //表示该注解会在Class字节码文件中存在,在运行时可以通过反射获取到
@Documented //该注解将被包含在JavaDoc中
public @interface FeignClient {
@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
String url() default "";
boolean decode404() default false;
Class>[] configuration() default {};
Class> fallback() default void.class;
Class> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
@FeignClient注解用于创建声明式API接口,该接口是RESTFul风格的。在源码中,value()和name()一样,是被调用服务的ServiceId。url()直接填写硬编码的URL地址,decode404()是被解码,还是抛异常。configuration()指明FeignClient的配置类,默认的配置类为FeignClientsConfiguration类, 在缺省的情况下,这个类默认注入默认的Decoder,Encoder等配置类。fallback()是配置熔断器的处理类。
Feign通过处理注解生成Request模板,从而简化HTTP API的开发。开发人员可以使用注解的方式定制Request API模板。在发送Http Request请求之前,Feign通过处理注解的方法替换到Request模板的参数,生成真正的Request,并交给Java Http客户端去处理。利用这种方式,开发者只需要关注Feign注解模板的开发,而不用关注Http请求本身,简化HTTP请求的过程。
Feign通过包扫描注入FeignClient的Bean,该源码在FeignClientRegister类中。首先在程序启动时,会检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解的接口,代码如下:
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map 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();
}
this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
}
}
当程序的启动类上有@EnableFeignClients注解。在程序启动后,会通过包扫描将有@FeignClient注解修饰的接口连接口名和注解信息一起取出,赋值给BeanDefinitionBuilder,然后根据BenDefinition得到BeanDeifinition,注入IOC容器中。
//扫描FeignClient注解的
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
scanner.setResourceLoader(this.resourceLoader);
Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
Class>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
Object basePackages;
if (clients != null && clients.length != 0) {
final Set clientClasses = new HashSet();
basePackages = new HashSet();
Class[] var9 = clients;
int var10 = clients.length;
for(int var11 = 0; var11 < var10; ++var11) {
Class> clazz = var9[var11];
((Set)basePackages).add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
} else {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = this.getBasePackages(metadata);
}
Iterator var17 = ((Set)basePackages).iterator();
while(var17.hasNext()) {
String basePackage = (String)var17.next();
Set candidateComponents = scanner.findCandidateComponents(basePackage);
Iterator var21 = candidateComponents.iterator();
while(var21.hasNext()) {
BeanDefinition candidateComponent = (BeanDefinition)var21.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
Map attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = this.getClientName(attributes);
this.registerClientConfiguration(registry, name, attributes.get("configuration"));
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
//获取BeanDeifition 注入IOC容器中
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
this.validate(attributes);
definition.addPropertyValue("url", this.getUrl(attributes));
definition.addPropertyValue("path", this.getPath(attributes));
String name = this.getName(attributes);
definition.addPropertyValue("name", name);
String contextId = this.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(2);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = this.getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
注入BeanDefinition之后,通过JDK代理,当调用FeignClient 接口中的方法,该方法被拦截,源码在ReflectiveFeign类,源码如下:
public T newInstance(Target target) {
Map nameToHandler = this.targetToHandlersByName.apply(target);
Map methodToHandler = new LinkedHashMap();
List defaultMethodHandlers = new LinkedList();
Method[] var5 = target.type().getMethods();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
Method method = var5[var7];
if (method.getDeclaringClass() != Object.class) {
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 handler = this.factory.create(target, methodToHandler);
T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
Iterator var12 = defaultMethodHandlers.iterator();
while(var12.hasNext()) {
DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
在SynchronousMethodHandler类进行拦截处理,会根据参数生成RequestTemplate对象,该对象时Http请求的模板,代码如下:
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
return this.executeAndDecode(template, options);
} catch (RetryableException var9) {
RetryableException e = var9;
try {
retryer.continueOrPropagate(e);
} catch (RetryableException var8) {
Throwable cause = var8.getCause();
if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
throw cause;
}
throw var8;
}
if (this.logLevel != Level.NONE) {
this.logger.logRetry(this.metadata.configKey(), this.logLevel);
}
}
}
}
有一个executeAndDecode方法,该方法通过RequestTemplate生成Request请求对象,然后通过Http Client获取Response,即通过Http Client来获取响应。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
response = this.client.execute(request, options);
} catch (IOException var16) {
if (this.logLevel != Level.NONE) {
this.logger.logIOException(this.metadata.configKey(), this.logLevel, var16, this.elapsedTime(start));
}
throw FeignException.errorExecuting(request, var16);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
boolean shouldClose = true;
//省略代码
}
在Feign中Client是一个非常重要的组件,Feign最终发送Requet请求以及接受Response响应都是由Client组件完成的。Client在Feign源码中是一个接口,在默认情况下,Client实现类默认是Client.Default,Client.Default是由HttpUrlConnection来实现网络请求的。另外,Client还支持HttpClient和OkhHttp进行网络请求。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
在上面我们知道了默认的使用Client.Default,Default使用的HttpUrlConnection。如何在Feign中使用HttpClient或者OkhHttp网络请求框架呢?查看上面FeignAutoConfiguration的源码:
@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;
protected HttpClientFeignConfiguration() {
}
...
}
@ConditionalOnClass({ApacheHttpClient.class}),只需要在pom文件上加上HttpClient的Classpath即可。另外在配置文件中applictaion.yml文件中配置feign.httpclient.enable为true,也可以不用写,因为@CondittionalProperty注解可知,默认值就是true。
同理OkhHttp也是同样配置。
那么Feign是如何实现负载均衡的?我们之前知道Client,它有三个实现类:
其中LoadBalancerFeignClient作为Ribbon负载均衡实现的客户端。Feign是如何注册LoadBanlancerFeignClient,看FeignRibbonClientAutoConfiguration:
@ConditionalOnClass({ILoadBalancer.class, Feign.class})
@ConditionalOnProperty(
value = {"spring.cloud.loadbalancer.ribbon.enabled"},
matchIfMissing = true
)
@Configuration(
proxyBeanMethods = false
)
@AutoConfigureBefore({FeignAutoConfiguration.class})
@EnableConfigurationProperties({FeignHttpClientProperties.class})
@Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class})
public class FeignRibbonClientAutoConfiguration {
public FeignRibbonClientAutoConfiguration() {
}
@Bean
@Primary
@ConditionalOnMissingBean
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
public CachingSpringLoadBalancerFactory cachingLBClientFactory(SpringClientFactory factory) {
return new CachingSpringLoadBalancerFactory(factory);
}
@Bean
@Primary
@ConditionalOnMissingBean
@ConditionalOnClass(
name = {"org.springframework.retry.support.RetryTemplate"}
)
public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
return new CachingSpringLoadBalancerFactory(factory, retryFactory);
}
@Bean
@ConditionalOnMissingBean
public Options feignRequestOptions() {
return LoadBalancerFeignClient.DEFAULT_OPTIONS;
}
}
@ConditionalOnClass({ILoadBalancer.class, Feign.class}) 其中ILoadBalancer.class是Ribbon依赖的类,换句话说该配置需要引入Ribbon和Feign依赖才会生效
@AutoConfigureBefore({FeignAutoConfiguration.class}) 如果该自动配置类生效,则在FeignAutoConfiguration之前进行配置,因为FeignAutoConfiguration关联到Feigin Client代理对象的实例化,而其中真是发生请求调用的Client 对线实例在Feign Client调用Feign.build进行实例化Client实现对象,需要提前实例化对象。换句话来说,如果Client对应Ribbon的实现类LoadBalancerFeignClient存在,就是用Ribbon的负载均衡客户端进行调用处理,反之,使用默认的feign.Client.Default。
@Import({DefaultFeignLoadBalancedConfiguration.class})
DefaultFeignLoadBalancedConfiguration 是一个 Bean 配置类,对 LoadBalancerFeignClient 进行了实例化配置。
总结
1、 Feign Client 在执行调用最终执行的是 Client#execute。
2、 Client 是一个接口,其实现类 LoadBalancerFeignClient 和 Default。在 Feign 代理对象实例化作为属性存入 Feign Client 代理对象中。
3、 当应用依赖中引入 Ribbon 相关依赖时,在 Feign 代理对象实例化前,会先生成 Client Ribbon 实现类 LoadBalancerFeignClient 的实例对象。反之,使用默认的 feign.Default 。
查看LoadBalancerFeignClient类中的execute方法,即执行请求的方法:
public Response execute(Request request, Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = this.getClientConfig(options, clientName);
return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
} catch (ClientException var8) {
IOException io = this.findIOException(var8);
if (io != null) {
throw io;
} else {
throw new RuntimeException(var8);
}
}
}
其中的executeWithLoadBalancer方法,即通过负载均衡的方式进行网络请求,代码如下:
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand command = this.buildLoadBalancerCommand(request, requestConfig);
try {
return (IResponse)command.submit(new ServerOperation() {
public Observable call(Server server) {
URI finalUri = AbstractLoadBalancerAwareClient.this.reconstructURIWithServer(server, request.getUri());
ClientRequest requestForServer = request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
} catch (Exception var5) {
return Observable.error(var5);
}
}
}).toBlocking().single();
} catch (Exception var6) {
Throwable t = var6.getCause();
if (t instanceof ClientException) {
throw (ClientException)t;
} else {
throw new ClientException(var6);
}
}
}
进入这里面的summit方法,进入这个submit可以看出是LoadBalancerCommand类的方法:
public Observable submit(final ServerOperation operation) {
//省略代码
Observable o = (this.server == null ? this.selectServer() : Observable.just(this.server)).concatMap(new Func1>() {
public Observable call(Server server) {
//.....
}
有一个selectServer方法,该方法时选择负载均衡的方法,代码如下:
private Observable selectServer() {
return Observable.create(new OnSubscribe() {
public void call(Subscriber super Server> next) {
try {
Server server = LoadBalancerCommand.this.loadBalancerContext.getServerFromLoadBalancer(LoadBalancerCommand.this.loadBalancerURI, LoadBalancerCommand.this.loadBalancerKey);
next.onNext(server);
next.onCompleted();
} catch (Exception var3) {
next.onError(var3);
}
}
});
}
最终负载均衡交给LoadBalancerContext来处理。
总结:
首先通过@EnableFeignClients注解开启FeignClient功能,只有这个注解存在,才会在程序中开启对@FeignClient注解的包扫描
根据FeinClient规则实现接口,并在接口加上@FeignClient注解
程序启动后,会进行包扫描,扫描被@FeignClient注解的类,并将这些信息注入IOC容器中
当接口的方法被调用的时候,通过JDK代理对象生成具体的RestTemplate模板对象
根据RestTemplate再生成HTTP请求的Request对象
Request交给Client去处理,Client的网络请求框架可以使HTTPURLConnection,HttpClient和OkhHttp
最后Client被封装到LoadBalancerFeignClient,这个类结合了Ribbon做到了负载均衡
Feign日志等级源码如下:
public enum Level {
NONE, //不输出日志
BASIC, //只输出方法的URL和响应状态码以及接口的执行时间
HEADERS, //将BASIC信息和请求头信息输出
FULL //输出完整的请求信息
}
自定义一个配置类:
@Configuration
public class FeignConfiguration {
/**
* 日志级别
*
* @return
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
需要在Feign Client中的@FeignClient增加配置类:
@FeignClient(value = "eureka-client-user-service", configuration = FeignConfiguration. class)
public interface UserRemoteClient {
// ...
}
在配置文件中执行 Client 的日志级别才能正常输出日志,格式是“logging.level.client 类地址=级别”
logging.level.net.biancheng.feign_demo.remote.UserRemoteClient=DEBUG
其他自定义配置:自定义配置