一个成熟的微服务集群,内部调用必然依赖一个好的 RPC 框架,比如:基于 http 协议的 feign,基于私有 tcp 协议的 dubbo。本文内容介绍 feign。
一、What?
如果不使用 rpc 框架,那么调用服务需要走 http 的话,配置请求 head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。
Feign 是一个 http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求。Feign 通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了 http 调用流程。
二、How?
feign 底层基于 http 协议,适应绝大部分内外部 API 调用的应用场景,并且 SpringCloud 对 feign 已经有了比较好的封装。使用上可以依赖于 SpringCloud 封装过的 feign:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送 HTTP 请求,没有连接池,但是对每个地址会保持一个长连接,即利用 HTTP 的
persistence connection。建议替换为 Apache HttpClient,作为底层的 http client 包,从而获取连接池、超时时间等与性能息息相关的控制能力:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
在配置文件中启用 ApacheHttpClient:
feign.httpclient.enabled=true
FeignClient 参数:
public @interface FeignClient {@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
String contextId() default "";
// 指定 FeignClient 的名称
@AliasFor("value")
String name() default "";
String qualifier() default "";
// 全路径地址或 hostname,http 或 https 可选
String url() default "";
// 当发生 http 404 错误时,如果该字段位 true,会调用 decoder 进行解码,否则抛出 FeignException
boolean decode404() default false;
// Feign 配置类,可以自定义 Feign 的 LogLevel
Class<?>[] configuration() default {};
// 容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑
Class<?> fallback() default void.class;
// 工厂类,用于生成 fallback 类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
Class<?> fallbackFactory() default void.class;
// 定义当前 FeignClient 的统一前缀,类似于 controller 类上的 requestMapping
String path() default "";
boolean primary() default true;}
三、Why?
- 启动时,程序会进行包扫描,扫描所有包下所有 @FeignClient 注解的类,并将这些类注入到 spring 的 IOC 容器中。
- 当定义的 Feign 中的接口被调用时,通过 JDK 的动态代理来生成 RequestTemplate。RequestTemplate 中包含请求的所有信息,如请求参数,请求 URL 等。
- RequestTemplate 声场 Request,然后将 Request 交给 client 处理,client 默认是 JDK 的 HTTPUrlConnection,也可以是 OKhttp、Apache 的 HTTPClient 等。
- 最后 client 封装成 LoadBaLanceClient,结合 ribbon 负载均衡地发起调用。
使用 Feign 涉及两个注解:@EnableFeignClients,用来开启 Feign;@FeignClient,标记要用 Feign 来拦截的请求接口。
1、启用
启动配置上检查是否有 @EnableFeignClients 注解,如果有该注解,则开启包扫描,扫描被 @FeignClient 注解的接口。扫描出该注解后,
通过 beanDefinition 注入到 IOC 容器中,方便后续被调用使用。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};}
@EnableFeignClients 是关于注解扫描的配置,使用了 @Import(FeignClientsRegistrar.class)。在 spring context 处理过程中,这个 Import 会在解析 Configuration 的时候当做提供了其他的 bean definition 的扩展,Spring 通过调用其 registerBeanDefinitions 方法来获取其提供的 bean definition。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
}
FeignClientsRegistrar 里重写了 spring 里 ImportBeanDefinitionRegistrar 接口的 registerBeanDefinitions 方法。也就是在启动时,处理了 EnableFeignClients 注解后,registry 里面会多出一些关于 Feign 的 BeanDefinition。
2、发起请求
ReflectiveFeign 内部使用了 jdk 的动态代理为目标接口生成了一个动态代理类,这里会生成一个 InvocationHandler 统一的方法拦截器,同时为接口的每个方法生成一个 SynchronousMethodHandler 拦截器,并解析方法上的元数据,生成一个 http 请求模板 RequestTemplate。
public class ReflectiveFeign extends Feign {
@Override
public <T> T newInstance(Target<T> target) {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()) {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 handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
}
Feign 内置了一个重试器,当 HTTP 请求出现 IO 异常时,Feign 会有一个最大尝试次数发送请求:
final class SynchronousMethodHandler implements MethodHandler {
@Override
public Object invoke(Object[] argv) throws Throwable {
// 根据输入参数,构造 Http 请求
RequestTemplate template = buildTemplateFromArgs.create(argv);
// 克隆出一份重试器
Retryer retryer = this.retryer.clone();
// 尝试最大次数,如果中间有结果,直接返回
while (true) {
try {return executeAndDecode(template);
} catch (RetryableException e) {
try {retryer.continueOrPropagate(e);
} catch (RetryableException th) {Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}
}
if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
}
Feign 真正发送 HTTP 请求是委托给 feign.Client 来做的:
public interface Client {Response execute(Request request, Options options) throws IOException;
class Default implements Client {
@Override
public Response execute(Request request, Options options) throws IOException {HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
}
}
默认底层通过 JDK 的 java.net.HttpURLConnection 实现了 feign.Client 接口类。在每次发送请求的时候,都会创建新的 HttpURLConnection 链接,这样的话默认情况下 Feign 的性能很差,一般扩展该接口,比如使用 Apache 的 HttpClient 或者 OkHttp3 等基于连接池的高性能 Http 客户端。
注意:SynchronousMethodHandler 并不是直接完成远程 URL 的请求,而是通过负载均衡机制,定位到合适的远程 server 服务器,然后再完成真正的远程 URL 请求。即:SynchronousMethodHandler 实例的 client 成员,其实际不是 feign.Client.Default 类型,而是 LoadBalancerFeignClient 客户端负载均衡类型。
3、性能分析
Feign 框架比较小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理 Http 请求的环节。可以从这个方面着手分析系统的性能提升点。